chore: implemented CRUD operations in all the layouts (#2505)

* chore: basic crud operations added to the list view

* refactor: cycle details page

* refactor: module details page

* chore: added quick actions to kanban issue block

* chore: implement quick actions in calendar layout

* fix: custom menu component

* chore: separate quick action dropdowns implemented

* style: loader for calendar

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr"; import { mutate } from "swr";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// headless ui
import { Disclosure, Popover, Transition } from "@headlessui/react"; import { Disclosure, Popover, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// hooks // hooks
@ -28,32 +30,39 @@ import {
AlertCircle, AlertCircle,
} from "lucide-react"; } from "lucide-react";
// helpers // helpers
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper";
import { isDateGreaterThanToday, renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; import {
getDateRangeStatus,
isDateGreaterThanToday,
renderDateFormat,
renderShortDateWithYearFormat,
} from "helpers/date-time.helper";
// types // types
import { IUser, ICycle } from "types"; import { ICycle } from "types";
// fetch-keys // fetch-keys
import { CYCLE_DETAILS } from "constants/fetch-keys"; import { CYCLE_DETAILS } from "constants/fetch-keys";
type Props = { type Props = {
cycle: ICycle | undefined;
isOpen: boolean; isOpen: boolean;
cycleStatus: string; cycleId: string;
isCompleted: boolean;
user: IUser | undefined;
}; };
// services
const cycleService = new CycleService(); const cycleService = new CycleService();
export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatus, isCompleted, user }) => { // TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { isOpen, cycleId } = props;
const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query as { const { workspaceSlug, projectId } = router.query;
workspaceSlug: string;
projectId: string; const { user: userStore, cycle: cycleDetailsStore } = useMobxStore();
cycleId: string;
}; const user = userStore.currentUser ?? undefined;
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -78,9 +87,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
}; };
const handleCopyText = () => { const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`)
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -96,11 +103,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
}; };
useEffect(() => { useEffect(() => {
if (cycle) if (cycleDetails)
reset({ reset({
...cycle, ...cycleDetails,
}); });
}, [cycle, reset]); }, [cycleDetails, reset]);
const dateChecker = async (payload: any) => { const dateChecker = async (payload: any) => {
try { try {
@ -129,11 +136,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
return; return;
} }
if (cycle?.start_date && cycle?.end_date) { if (cycleDetails?.start_date && cycleDetails?.end_date) {
const isDateValidForExistingCycle = await dateChecker({ const isDateValidForExistingCycle = await dateChecker({
start_date: `${watch("start_date")}`, start_date: `${watch("start_date")}`,
end_date: `${watch("end_date")}`, end_date: `${watch("end_date")}`,
cycle_id: cycle.id, cycle_id: cycleDetails.id,
}); });
if (isDateValidForExistingCycle) { if (isDateValidForExistingCycle) {
@ -203,11 +210,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
return; return;
} }
if (cycle?.start_date && cycle?.end_date) { if (cycleDetails?.start_date && cycleDetails?.end_date) {
const isDateValidForExistingCycle = await dateChecker({ const isDateValidForExistingCycle = await dateChecker({
start_date: `${watch("start_date")}`, start_date: `${watch("start_date")}`,
end_date: `${watch("end_date")}`, end_date: `${watch("end_date")}`,
cycle_id: cycle.id, cycle_id: cycleDetails.id,
}); });
if (isDateValidForExistingCycle) { if (isDateValidForExistingCycle) {
@ -258,29 +265,39 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
} }
}; };
const isStartValid = new Date(`${cycle?.start_date}`) <= new Date(); const cycleStatus =
const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`); cycleDetails?.start_date && cycleDetails?.end_date
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
: "draft";
const isCompleted = cycleStatus === "completed";
const progressPercentage = cycle ? Math.round((cycle.completed_issues / cycle.total_issues) * 100) : null; const isStartValid = new Date(`${cycleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
const progressPercentage = cycleDetails
? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
: null;
if (!cycleDetails) return null;
return ( return (
<> <>
{cycle && ( {cycleDetails && workspaceSlug && projectId && (
<CycleDeleteModal <CycleDeleteModal
cycle={cycle} cycle={cycleDetails}
modal={cycleDeleteModal} modal={cycleDeleteModal}
modalClose={() => setCycleDeleteModal(false)} modalClose={() => setCycleDeleteModal(false)}
onSubmit={() => {}} onSubmit={() => {}}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug.toString()}
projectId={projectId} projectId={projectId.toString()}
/> />
)} )}
<div <div
className={`fixed top-[66px] z-20 ${ className={`absolute top-0 z-20 ${
isOpen ? "right-0" : "-right-[24rem]" isOpen ? "right-0" : "-right-[24rem]"
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-200 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`} } h-full w-[24rem] overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
> >
{cycle ? ( {cycleDetails ? (
<> <>
<div className="flex flex-col items-start justify-center"> <div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm"> <div className="flex gap-2.5 px-5 text-sm">
@ -296,13 +313,13 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} disabled={isCompleted ?? false}
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${ className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
cycle.start_date ? "" : "text-custom-text-200" cycleDetails.start_date ? "" : "text-custom-text-200"
}`} }`}
> >
<CalendarDays className="h-3 w-3" /> <CalendarDays className="h-3 w-3" />
<span> <span>
{renderShortDateWithYearFormat( {renderShortDateWithYearFormat(
new Date(`${watch("start_date") ? watch("start_date") : cycle?.start_date}`), new Date(`${watch("start_date") ? watch("start_date") : cycleDetails?.start_date}`),
"Start date" "Start date"
)} )}
</span> </span>
@ -319,7 +336,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
> >
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden"> <Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker <CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : cycle?.start_date} value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => { onChange={(val) => {
if (val) { if (val) {
handleStartDateChange(val); handleStartDateChange(val);
@ -344,14 +361,14 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} disabled={isCompleted ?? false}
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${ className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
cycle.end_date ? "" : "text-custom-text-200" cycleDetails.end_date ? "" : "text-custom-text-200"
}`} }`}
> >
<CalendarDays className="h-3 w-3" /> <CalendarDays className="h-3 w-3" />
<span> <span>
{renderShortDateWithYearFormat( {renderShortDateWithYearFormat(
new Date(`${watch("end_date") ? watch("end_date") : cycle?.end_date}`), new Date(`${watch("end_date") ? watch("end_date") : cycleDetails?.end_date}`),
"End date" "End date"
)} )}
</span> </span>
@ -368,7 +385,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
> >
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden"> <Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker <CustomRangeDatePicker
value={watch("end_date") ? watch("end_date") : cycle?.end_date} value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => { onChange={(val) => {
if (val) { if (val) {
handleEndDateChange(val); handleEndDateChange(val);
@ -391,7 +408,9 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<div className="flex w-full flex-col items-start justify-start gap-2"> <div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2"> <div className="flex w-full items-start justify-between gap-2">
<div className="max-w-[300px]"> <div className="max-w-[300px]">
<h4 className="text-xl font-semibold text-custom-text-100 break-words w-full">{cycle.name}</h4> <h4 className="text-xl font-semibold text-custom-text-100 break-words w-full">
{cycleDetails.name}
</h4>
</div> </div>
<CustomMenu width="lg" ellipsis> <CustomMenu width="lg" ellipsis>
{!isCompleted && ( {!isCompleted && (
@ -412,7 +431,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div> </div>
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full"> <span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
{cycle.description} {cycleDetails.description}
</span> </span>
</div> </div>
@ -424,20 +443,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div> </div>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( {cycleDetails.owned_by.avatar && cycleDetails.owned_by.avatar !== "" ? (
<img <img
src={cycle.owned_by.avatar} src={cycleDetails.owned_by.avatar}
height={12} height={12}
width={12} width={12}
className="rounded-full" className="rounded-full"
alt={cycle.owned_by.display_name} alt={cycleDetails.owned_by.display_name}
/> />
) : ( ) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white"> <span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
{cycle.owned_by.display_name.charAt(0)} {cycleDetails.owned_by.display_name.charAt(0)}
</span> </span>
)} )}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span> <span className="text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
</div> </div>
</div> </div>
@ -449,9 +468,9 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<div className="flex items-center gap-2.5 text-custom-text-200"> <div className="flex items-center gap-2.5 text-custom-text-200">
<span className="h-4 w-4"> <span className="h-4 w-4">
<ProgressBar value={cycle.completed_issues} maxValue={cycle.total_issues} /> <ProgressBar value={cycleDetails.completed_issues} maxValue={cycleDetails.total_issues} />
</span> </span>
{cycle.completed_issues}/{cycle.total_issues} {cycleDetails.completed_issues}/{cycleDetails.total_issues}
</div> </div>
</div> </div>
</div> </div>
@ -498,7 +517,8 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</span> </span>
<span> <span>
Pending Issues -{" "} Pending Issues -{" "}
{cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)} {cycleDetails.total_issues -
(cycleDetails.completed_issues + cycleDetails.cancelled_issues)}
</span> </span>
</div> </div>
@ -515,10 +535,10 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div> </div>
<div className="relative"> <div className="relative">
<ProgressChart <ProgressChart
distribution={cycle.distribution.completion_chart} distribution={cycleDetails.distribution.completion_chart}
startDate={cycle.start_date ?? ""} startDate={cycleDetails.start_date ?? ""}
endDate={cycle.end_date ?? ""} endDate={cycleDetails.end_date ?? ""}
totalIssues={cycle.total_issues} totalIssues={cycleDetails.total_issues}
/> />
</div> </div>
</div> </div>
@ -540,7 +560,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
<span className="font-medium text-custom-text-200">Other Information</span> <span className="font-medium text-custom-text-200">Other Information</span>
</div> </div>
{cycle.total_issues > 0 ? ( {cycleDetails.total_issues > 0 ? (
<Disclosure.Button> <Disclosure.Button>
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" /> <ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</Disclosure.Button> </Disclosure.Button>
@ -555,18 +575,18 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div> </div>
<Transition show={open}> <Transition show={open}>
<Disclosure.Panel> <Disclosure.Panel>
{cycle.total_issues > 0 ? ( {cycleDetails.total_issues > 0 ? (
<div className="h-full w-full py-4"> <div className="h-full w-full py-4">
<SidebarProgressStats <SidebarProgressStats
distribution={cycle.distribution} distribution={cycleDetails.distribution}
groupedIssues={{ groupedIssues={{
backlog: cycle.backlog_issues, backlog: cycleDetails.backlog_issues,
unstarted: cycle.unstarted_issues, unstarted: cycleDetails.unstarted_issues,
started: cycle.started_issues, started: cycleDetails.started_issues,
completed: cycle.completed_issues, completed: cycleDetails.completed_issues,
cancelled: cycle.cancelled_issues, cancelled: cycleDetails.cancelled_issues,
}} }}
totalIssues={cycle.total_issues} totalIssues={cycleDetails.total_issues}
/> />
</div> </div>
) : ( ) : (
@ -595,4 +615,4 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ cycle, isOpen, cycleStatu
</div> </div>
</> </>
); );
}; });

View File

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

View File

@ -1,16 +1,20 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components // components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { ProjectAnalyticsModal } from "components/analytics"; import { ProjectAnalyticsModal } from "components/analytics";
// ui // ui
import { Button } from "@plane/ui"; import { Breadcrumbs, Button, CustomMenu } from "@plane/ui";
// icons // icons
import { Plus } from "lucide-react"; import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
// helpers
import { truncateText } from "helpers/string.helper";
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants // constants
@ -28,9 +32,15 @@ export const CycleIssuesHeader: React.FC = observer(() => {
cycleIssueFilter: cycleIssueFilterStore, cycleIssueFilter: cycleIssueFilterStore,
project: projectStore, project: projectStore,
} = useMobxStore(); } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilterStore.userDisplayFilters.layout;
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
const toggleSidebar = () => {
setValue(`${!isSidebarCollapsed}`);
};
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -88,6 +98,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
[issueFilterStore, projectId, workspaceSlug] [issueFilterStore, projectId, workspaceSlug]
); );
const cyclesList = projectId ? cycleStore.cycles[projectId.toString()] : undefined;
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
return ( return (
@ -97,50 +108,91 @@ export const CycleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)} onClose={() => setAnalyticsModal(false)}
cycleDetails={cycleDetails ?? undefined} cycleDetails={cycleDetails ?? undefined}
/> />
<div className="flex items-center gap-2"> <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">
<LayoutSelection <div className="flex items-center gap-2">
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} <Breadcrumbs onBack={() => router.back()}>
onChange={(layout) => handleLayoutChange(layout)} <Breadcrumbs.BreadcrumbItem
selectedLayout={activeLayout} link={
/> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles`}>
<FiltersDropdown title="Filters"> <a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
<FilterSelection <p className="truncate">{`${truncateText(cycleDetails?.project_detail.name ?? "", 32)} Cycles`}</p>
filters={cycleIssueFilterStore.cycleFilters} </a>
handleFiltersUpdate={handleFiltersUpdate} </Link>
layoutDisplayFiltersOptions={ }
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined />
</Breadcrumbs>
<CustomMenu
label={
<>
<ContrastIcon className="h-3 w-3" />
{cycleDetails?.name && truncateText(cycleDetails.name, 40)}
</>
} }
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} className="ml-1.5 flex-shrink-0"
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} width="auto"
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} >
{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="Filters">
<FiltersDropdown title="Display"> <FilterSelection
<DisplayFiltersSelection filters={cycleIssueFilterStore.cycleFilters}
displayFilters={issueFilterStore.userDisplayFilters} handleFiltersUpdate={handleFiltersUpdate}
displayProperties={issueFilterStore.userDisplayProperties} layoutDisplayFiltersOptions={
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate} activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate} }
layoutDisplayFiltersOptions={ labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
} states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/> />
</FiltersDropdown> </FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <FiltersDropdown title="Display">
Analytics <DisplayFiltersSelection
</Button> displayFilters={issueFilterStore.userDisplayFilters}
<Button displayProperties={issueFilterStore.userDisplayProperties}
onClick={() => { handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
const e = new KeyboardEvent("keydown", { handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
key: "c", layoutDisplayFiltersOptions={
}); activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
document.dispatchEvent(e); }
}} />
size="sm" </FiltersDropdown>
prependIcon={<Plus />} <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
> Analytics
Add Issue </Button>
</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> </div>
</> </>
); );

View File

@ -1,16 +1,20 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components // components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { ProjectAnalyticsModal } from "components/analytics"; import { ProjectAnalyticsModal } from "components/analytics";
// ui // ui
import { Button } from "@plane/ui"; import { Breadcrumbs, Button, CustomMenu } from "@plane/ui";
// icons // icons
import { Plus } from "lucide-react"; import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
// helpers
import { truncateText } from "helpers/string.helper";
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants // constants
@ -28,9 +32,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
moduleFilter: moduleFilterStore, moduleFilter: moduleFilterStore,
project: projectStore, project: projectStore,
} = useMobxStore(); } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilterStore.userDisplayFilters.layout;
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
const toggleSidebar = () => {
setValue(`${!isSidebarCollapsed}`);
};
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -88,6 +98,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
[issueFilterStore, projectId, workspaceSlug] [issueFilterStore, projectId, workspaceSlug]
); );
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined; const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
return ( return (
@ -97,50 +108,94 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)} onClose={() => setAnalyticsModal(false)}
moduleDetails={moduleDetails ?? undefined} moduleDetails={moduleDetails ?? undefined}
/> />
<div className="flex items-center gap-2"> <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">
<LayoutSelection <div className="flex items-center gap-2">
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} <Breadcrumbs onBack={() => router.back()}>
onChange={(layout) => handleLayoutChange(layout)} <Breadcrumbs.BreadcrumbItem
selectedLayout={activeLayout} link={
/> <Link href={`/${workspaceSlug}/projects/${projectId}/modules`}>
<FiltersDropdown title="Filters"> <a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
<FilterSelection <p className="truncate">{`${truncateText(
filters={moduleFilterStore.moduleFilters} moduleDetails?.project_detail.name ?? "",
handleFiltersUpdate={handleFiltersUpdate} 32
layoutDisplayFiltersOptions={ )} Modules`}</p>
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined </a>
</Link>
}
/>
</Breadcrumbs>
<CustomMenu
label={
<>
<ContrastIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</>
} }
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} className="ml-1.5 flex-shrink-0"
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} width="auto"
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} >
{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="Filters">
<FiltersDropdown title="Display"> <FilterSelection
<DisplayFiltersSelection filters={moduleFilterStore.moduleFilters}
displayFilters={issueFilterStore.userDisplayFilters} handleFiltersUpdate={handleFiltersUpdate}
displayProperties={issueFilterStore.userDisplayProperties} layoutDisplayFiltersOptions={
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate} activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate} }
layoutDisplayFiltersOptions={ labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
} states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/> />
</FiltersDropdown> </FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <FiltersDropdown title="Display">
Analytics <DisplayFiltersSelection
</Button> displayFilters={issueFilterStore.userDisplayFilters}
<Button displayProperties={issueFilterStore.userDisplayProperties}
onClick={() => { handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
const e = new KeyboardEvent("keydown", { handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
key: "c", layoutDisplayFiltersOptions={
}); activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
document.dispatchEvent(e); }
}} />
size="sm" </FiltersDropdown>
prependIcon={<Plus />} <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
> Analytics
Add Issue </Button>
</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> </div>
</> </>
); );

View File

@ -1,56 +1,31 @@
import { useEffect, useState, Fragment } from "react"; import { useEffect, useState, Fragment } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services
import { IssueService, IssueArchiveService } from "services/issue";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useToast from "hooks/use-toast";
// icons
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import type { IIssue, IUser, ISubIssueResponse } from "types"; import type { IIssue } from "types";
// fetch-keys
import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
} from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
data: IIssue | null; data: IIssue;
user: IUser | undefined;
onSubmit?: () => Promise<void>; onSubmit?: () => Promise<void>;
redirection?: boolean;
}; };
const issueService = new IssueService(); export const DeleteIssueModal: React.FC<Props> = observer((props) => {
const issueArchiveService = new IssueArchiveService(); const { data, isOpen, handleClose, onSubmit } = props;
export const DeleteIssueModal: React.FC<Props> = ({
isOpen,
handleClose,
data,
user,
onSubmit,
redirection = true,
}) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, issueId } = router.query; const { workspaceSlug } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const { displayFilters, params } = useIssuesView(); const { issueDetail: issueDetailStore } = useMobxStore();
const { setToastAlert } = useToast(); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
useEffect(() => { useEffect(() => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
@ -61,77 +36,16 @@ export const DeleteIssueModal: React.FC<Props> = ({
handleClose(); handleClose();
}; };
const handleDeletion = async () => { const handleIssueDelete = async () => {
if (!workspaceSlug || !data) return; if (!workspaceSlug) return;
setIsDeleteLoading(true); setIsDeleteLoading(true);
await issueService await issueDetailStore.deleteIssue(workspaceSlug.toString(), data.project, data.id);
.deleteIssue(workspaceSlug as string, data.project, data.id, user)
.then(() => {
if (displayFilters.layout === "spreadsheet") {
if (data.parent) {
mutate<ISubIssueResponse>(
SUB_ISSUES(data.parent.toString()),
(prevData) => {
if (!prevData) return prevData;
const updatedArray = (prevData.sub_issues ?? []).filter((i) => i.id !== data.id);
return { if (onSubmit) await onSubmit().finally(() => setIsDeleteLoading(false));
...prevData,
sub_issues: updatedArray,
};
},
false
);
}
} else {
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, params));
}
if (onSubmit) onSubmit();
handleClose();
setToastAlert({
title: "Success",
type: "success",
message: "Issue deleted successfully",
});
if (issueId && redirection) router.back();
})
.catch(() => {
setIsDeleteLoading(false);
});
if (onSubmit) await onSubmit();
}; };
const handleArchivedIssueDeletion = async () => {
setIsDeleteLoading(true);
if (!workspaceSlug || !projectId || !data) return;
await issueArchiveService
.deleteArchivedIssue(workspaceSlug as string, projectId as string, data.id)
.then(() => {
mutate(PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
handleClose();
setToastAlert({
title: "Success",
type: "success",
message: "Issue deleted successfully",
});
router.back();
})
.catch((error) => {
console.log(error);
setIsDeleteLoading(false);
});
};
const handleIssueDelete = () => (isArchivedIssues ? handleArchivedIssueDeletion() : handleDeletion());
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}> <Dialog as="div" className="relative z-20" onClose={onClose}>
@ -194,4 +108,4 @@ export const DeleteIssueModal: React.FC<Props> = ({
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; });

View File

@ -9,14 +9,17 @@ import { Spinner } from "@plane/ui";
// types // types
import { ICalendarWeek } from "./types"; import { ICalendarWeek } from "./types";
import { IIssueGroupedStructure } from "store/issue"; import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
type Props = { type Props = {
issues: IIssueGroupedStructure | null; issues: IIssueGroupedStructure | null;
layout: "month" | "week" | undefined; layout: "month" | "week" | undefined;
showWeekends: boolean;
quickActions: (issue: IIssue) => React.ReactNode;
}; };
export const CalendarChart: React.FC<Props> = observer((props) => { export const CalendarChart: React.FC<Props> = observer((props) => {
const { issues, layout } = props; const { issues, layout, showWeekends, quickActions } = props;
const { calendar: calendarStore } = useMobxStore(); const { calendar: calendarStore } = useMobxStore();
@ -35,17 +38,17 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
<> <>
<div className="h-full w-full flex flex-col overflow-hidden"> <div className="h-full w-full flex flex-col overflow-hidden">
<CalendarHeader /> <CalendarHeader />
<CalendarWeekHeader /> <CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
<div className="h-full w-full overflow-y-auto"> <div className="h-full w-full overflow-y-auto">
{layout === "month" ? ( {layout === "month" ? (
<div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200"> <div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
{allWeeksOfActiveMonth && {allWeeksOfActiveMonth &&
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
<CalendarWeekDays key={weekIndex} week={week} issues={issues} /> <CalendarWeekDays key={weekIndex} week={week} issues={issues} quickActions={quickActions} />
))} ))}
</div> </div>
) : ( ) : (
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} /> <CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} quickActions={quickActions} />
)} )}
</div> </div>
</div> </div>

View File

@ -11,11 +11,16 @@ import { renderDateFormat } from "helpers/date-time.helper";
import { IIssueGroupedStructure } from "store/issue"; import { IIssueGroupedStructure } from "store/issue";
// constants // constants
import { MONTHS_LIST } from "constants/calendar"; import { MONTHS_LIST } from "constants/calendar";
import { IIssue } from "types";
type Props = { date: ICalendarDate; issues: IIssueGroupedStructure | null }; type Props = {
date: ICalendarDate;
issues: IIssueGroupedStructure | null;
quickActions: (issue: IIssue) => React.ReactNode;
};
export const CalendarDayTile: React.FC<Props> = observer((props) => { export const CalendarDayTile: React.FC<Props> = observer((props) => {
const { date, issues } = props; const { date, issues, quickActions } = props;
const { issueFilter: issueFilterStore } = useMobxStore(); const { issueFilter: issueFilterStore } = useMobxStore();
@ -48,7 +53,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "} {date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
{date.date.getDate()} {date.date.getDate()}
</div> </div>
<CalendarIssueBlocks issues={issuesList} /> <CalendarIssueBlocks issues={issuesList} quickActions={quickActions} />
{provided.placeholder} {provided.placeholder}
</> </>
</div> </div>

View File

@ -1,7 +1,7 @@
import React from "react"; import React, { useState } from "react";
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { usePopper } from "react-popper";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// icons // icons
@ -14,6 +14,21 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month"; const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "auto",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const { activeMonthDate } = calendarStore.calendarFilters; const { activeMonthDate } = calendarStore.calendarFilters;
const getWeekLayoutHeader = (): string => { const getWeekLayoutHeader = (): string => {
@ -47,10 +62,17 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
return ( return (
<Popover className="relative"> <Popover className="relative">
<Popover.Button className="outline-none text-xl font-semibold" disabled={calendarLayout === "week"}> <Popover.Button as={React.Fragment}>
{calendarLayout === "month" <button
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}` type="button"
: getWeekLayoutHeader()} 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> </Popover.Button>
<Transition <Transition
as={React.Fragment} as={React.Fragment}
@ -61,8 +83,13 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel> <Popover.Panel className="fixed z-50">
<div className="absolute left-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200"> <div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className="bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200"
>
<div className="flex items-center justify-between gap-2 pb-3"> <div className="flex items-center justify-between gap-2 pb-3">
<button <button
type="button" type="button"

View File

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

View File

@ -1,15 +1,17 @@
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Draggable } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Draggable } from "@hello-pangea/dnd";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
type Props = { issues: IIssue[] | null }; type Props = {
issues: IIssue[] | null;
quickActions: (issue: IIssue) => React.ReactNode;
};
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => { export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues } = props; const { issues, quickActions } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -21,7 +23,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{(provided, snapshot) => ( {(provided, snapshot) => (
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}> <Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
<a <a
className={`h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${ className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
snapshot.isDragging snapshot.isDragging
? "shadow-custom-shadow-rg bg-custom-background-90" ? "shadow-custom-shadow-rg bg-custom-background-90"
: "bg-custom-background-100 hover:bg-custom-background-90" : "bg-custom-background-100 hover:bg-custom-background-90"
@ -40,6 +42,12 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
<h6 className="text-xs flex-grow truncate">{issue.name}</h6> <h6 className="text-xs flex-grow truncate">{issue.name}</h6>
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
{/* <IssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/> */}
</a> </a>
</Link> </Link>
)} )}

View File

@ -1,15 +1,20 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart } from "components/issues"; import { CalendarChart, CycleIssueQuickActions } from "components/issues";
// types // types
import { IIssueGroupedStructure } from "store/issue"; import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const CycleCalendarLayout: React.FC = observer(() => { export const CycleCalendarLayout: React.FC = observer(() => {
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = useMobxStore(); const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug, cycleId } = router.query;
// TODO: add drag and drop functionality // TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {
@ -26,12 +31,43 @@ export const CycleCalendarLayout: React.FC = observer(() => {
const issues = cycleIssueStore.getIssues; const issues = cycleIssueStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete" | "remove") => {
if (!workspaceSlug || !cycleId) return;
if (action === "update") {
cycleIssueStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
}
if (action === "delete") cycleIssueStore.deleteIssue(date, null, issue);
if (action === "remove" && issue.bridge_id) {
cycleIssueStore.deleteIssue(date, null, issue);
cycleIssueStore.removeIssueFromCycle(
workspaceSlug.toString(),
issue.project,
cycleId.toString(),
issue.bridge_id
);
}
},
[cycleIssueStore, issueDetailStore, cycleId, workspaceSlug]
);
return ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<CalendarChart <CalendarChart
issues={issues as IIssueGroupedStructure | null} issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
handleRemoveFromCycle={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
/>
)}
/> />
</DragDropContext> </DragDropContext>
</div> </div>

View File

@ -1,15 +1,24 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart } from "components/issues"; import { CalendarChart, ModuleIssueQuickActions } from "components/issues";
// types // types
import { IIssueGroupedStructure } from "store/issue"; import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const ModuleCalendarLayout: React.FC = observer(() => { export const ModuleCalendarLayout: React.FC = observer(() => {
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore(); const {
moduleIssue: moduleIssueStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const router = useRouter();
const { workspaceSlug, moduleId } = router.query;
// TODO: add drag and drop functionality // TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {
@ -26,12 +35,45 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
const issues = moduleIssueStore.getIssues; const issues = moduleIssueStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete" | "remove") => {
if (!workspaceSlug || !moduleId) return;
if (action === "update") {
moduleIssueStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
moduleIssueStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
if (action === "remove" && issue.bridge_id) {
moduleIssueStore.deleteIssue(date, null, issue);
moduleIssueStore.removeIssueFromModule(
workspaceSlug.toString(),
issue.project,
moduleId.toString(),
issue.bridge_id
);
}
},
[moduleIssueStore, issueDetailStore, moduleId, workspaceSlug]
);
return ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<CalendarChart <CalendarChart
issues={issues as IIssueGroupedStructure | null} issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
handleRemoveFromModule={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
/>
)}
/> />
</DragDropContext> </DragDropContext>
</div> </div>

View File

@ -1,15 +1,20 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart } from "components/issues"; import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
// types // types
import { IIssueGroupedStructure } from "store/issue"; import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const CalendarLayout: React.FC = observer(() => { export const CalendarLayout: React.FC = observer(() => {
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore(); const { issue: issueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
// TODO: add drag and drop functionality // TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {
@ -26,12 +31,35 @@ export const CalendarLayout: React.FC = observer(() => {
const issues = issueStore.getIssues; const issues = issueStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete") => {
if (!workspaceSlug) return;
if (action === "update") {
issueStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
issueStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
},
[issueStore, issueDetailStore, workspaceSlug]
);
return ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<CalendarChart <CalendarChart
issues={issues as IIssueGroupedStructure | null} issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/>
)}
/> />
</DragDropContext> </DragDropContext>
</div> </div>

View File

@ -1,15 +1,24 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart } from "components/issues"; import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
// types // types
import { IIssueGroupedStructure } from "store/issue"; import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const ProjectViewCalendarLayout: React.FC = observer(() => { export const ProjectViewCalendarLayout: React.FC = observer(() => {
const { projectViewIssues: projectViewIssuesStore, issueFilter: issueFilterStore } = useMobxStore(); const {
projectViewIssues: projectViewIssuesStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
// TODO: add drag and drop functionality // TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {
@ -26,12 +35,35 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
const issues = projectViewIssuesStore.getIssues; const issues = projectViewIssuesStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete") => {
if (!workspaceSlug) return;
if (action === "update") {
projectViewIssuesStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
projectViewIssuesStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
},
[projectViewIssuesStore, issueDetailStore, workspaceSlug]
);
return ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<CalendarChart <CalendarChart
issues={issues as IIssueGroupedStructure | null} issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/>
)}
/> />
</DragDropContext> </DragDropContext>
</div> </div>

View File

@ -9,14 +9,16 @@ import { renderDateFormat } from "helpers/date-time.helper";
// types // types
import { ICalendarDate, ICalendarWeek } from "./types"; import { ICalendarDate, ICalendarWeek } from "./types";
import { IIssueGroupedStructure } from "store/issue"; import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
type Props = { type Props = {
issues: IIssueGroupedStructure | null; issues: IIssueGroupedStructure | null;
week: ICalendarWeek | undefined; week: ICalendarWeek | undefined;
quickActions: (issue: IIssue) => React.ReactNode;
}; };
export const CalendarWeekDays: React.FC<Props> = observer((props) => { export const CalendarWeekDays: React.FC<Props> = observer((props) => {
const { issues, week } = props; const { issues, week, quickActions } = props;
const { issueFilter: issueFilterStore } = useMobxStore(); const { issueFilter: issueFilterStore } = useMobxStore();
@ -34,7 +36,9 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
{Object.values(week).map((date: ICalendarDate) => { {Object.values(week).map((date: ICalendarDate) => {
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null; if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
return <CalendarDayTile key={renderDateFormat(date.date)} date={date} issues={issues} />; return (
<CalendarDayTile key={renderDateFormat(date.date)} date={date} issues={issues} quickActions={quickActions} />
);
})} })}
</div> </div>
); );

View File

@ -1,21 +1,25 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// constants // constants
import { DAYS_LIST } from "constants/calendar"; import { DAYS_LIST } from "constants/calendar";
export const CalendarWeekHeader: React.FC = observer(() => { type Props = {
const { issueFilter: issueFilterStore } = useMobxStore(); isLoading: boolean;
showWeekends: boolean;
};
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false; export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
const { isLoading, showWeekends } = props;
return ( return (
<div <div
className={`grid text-sm font-medium divide-x-[0.5px] divide-custom-border-200 ${ className={`relative grid text-sm font-medium divide-x-[0.5px] divide-custom-border-200 ${
showWeekends ? "grid-cols-7" : "grid-cols-5" showWeekends ? "grid-cols-7" : "grid-cols-5"
}`} }`}
> >
{isLoading && (
<div className="absolute h-[1.5px] w-3/4 bg-custom-primary-100 animate-[bar-loader_2s_linear_infinite]" />
)}
{Object.values(DAYS_LIST).map((day) => { {Object.values(DAYS_LIST).map((day) => {
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null; if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,73 +1,73 @@
// react beautiful dnd
import { Draggable } from "@hello-pangea/dnd"; import { Draggable } from "@hello-pangea/dnd";
// components // components
import { KanBanProperties } from "./properties"; import { KanBanProperties } from "./properties";
// types
import { IIssue } from "types";
interface IssueBlockProps { interface IssueBlockProps {
sub_group_id: string; sub_group_id: string;
columnId: string; columnId: string;
issues: any; index: number;
issue: IIssue;
isDragDisabled: boolean; isDragDisabled: boolean;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; handleIssues: (
display_properties: any; sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: any;
} }
export const IssueBlock = ({ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
sub_group_id, const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
columnId,
issues, const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
isDragDisabled, if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update");
handleIssues, };
display_properties,
}: IssueBlockProps) => ( return (
<> <>
{issues && issues.length > 0 ? ( <Draggable draggableId={issue.id} index={index} isDragDisabled={isDragDisabled}>
<> {(provided, snapshot) => (
{issues.map((issue: any, index: any) => ( <div
<Draggable className="group/kanban-block relative p-1.5 hover:cursor-default"
key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`} {...provided.draggableProps}
draggableId={`${issue.id}`} {...provided.dragHandleProps}
index={index} ref={provided.innerRef}
isDragDisabled={isDragDisabled}
> >
{(provided: any, snapshot: any) => ( <div className="absolute top-3 right-3 hidden group-hover/kanban-block:block">
<div {quickActions(
key={issue.id} !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
className="p-1.5 hover:cursor-default" !columnId && columnId === "null" ? null : columnId,
{...provided.draggableProps} issue
{...provided.dragHandleProps} )}
ref={provided.innerRef} </div>
> <div
<div className={`text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${
className={`text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${ snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent` }`}
}`} >
> {displayProperties && displayProperties?.key && (
{display_properties && display_properties?.key && ( <div className="text-xs line-clamp-1 text-custom-text-300">
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div> {issue.project_detail.identifier}-{issue.sequence_id}
)}
<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> </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> </div>
)} </div>
</Draggable> </div>
))} )}
</> </Draggable>
) : ( </>
!isDragDisabled && ( );
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center"> };
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)}
</>
);

View File

@ -0,0 +1,50 @@
// components
import { KanbanIssueBlock } from "components/issues";
import { IIssue } from "types";
interface IssueBlocksListProps {
sub_group_id: string;
columnId: string;
issues: IIssue[];
isDragDisabled: boolean;
handleIssues: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
}
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, display_properties } = props;
return (
<>
{issues && issues.length > 0 ? (
<>
{issues.map((issue, index) => (
<KanbanIssueBlock
key={`kanban-issue-block-${issue.id}`}
index={index}
issue={issue}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={display_properties}
columnId={columnId}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
/>
))}
</>
) : (
!isDragDisabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)}
</>
);
};

View File

@ -1,14 +1,15 @@
import React from "react"; import React, { useCallback } from "react";
// react beautiful dnd import { useRouter } from "next/router";
import { DragDropContext } from "@hello-pangea/dnd";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanSwimLanes } from "./swimlanes"; import { KanBanSwimLanes } from "./swimlanes";
import { KanBan } from "./default"; import { KanBan } from "./default";
// store import { CycleIssueQuickActions } from "components/issues";
import { useMobxStore } from "lib/mobx/store-provider"; // types
import { RootStore } from "store/root"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
@ -20,7 +21,11 @@ export const CycleKanBanLayout: React.FC = observer(() => {
cycleIssue: cycleIssueStore, cycleIssue: cycleIssueStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
cycleIssueKanBanView: cycleIssueKanBanViewStore, cycleIssueKanBanView: cycleIssueKanBanViewStore,
}: RootStore = useMobxStore(); issueDetail: issueDetailStore,
} = useMobxStore();
const router = useRouter();
const { workspaceSlug, cycleId } = router.query;
const issues = cycleIssueStore?.getIssues; const issues = cycleIssueStore?.getIssues;
@ -50,9 +55,27 @@ export const CycleKanBanLayout: React.FC = observer(() => {
: cycleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); : cycleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
}; };
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { const handleIssues = useCallback(
cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => {
}; if (!workspaceSlug || !cycleId) return;
if (action === "update") {
cycleIssueStore.updateIssueStructure(group_by, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
}
if (action === "delete") cycleIssueStore.deleteIssue(group_by, null, issue);
if (action === "remove" && issue.bridge_id) {
cycleIssueStore.deleteIssue(group_by, null, issue);
cycleIssueStore.removeIssueFromCycle(
workspaceSlug.toString(),
issue.project,
cycleId.toString(),
issue.bridge_id
);
}
},
[cycleIssueStore, issueDetailStore, cycleId, workspaceSlug]
);
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value); cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
@ -74,7 +97,15 @@ export const CycleKanBanLayout: React.FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -91,7 +122,15 @@ export const CycleKanBanLayout: React.FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}

View File

@ -1,16 +1,15 @@
import React from "react"; import React from "react";
// react beautiful dnd import { observer } from "mobx-react-lite";
import { Droppable } from "@hello-pangea/dnd"; import { Droppable } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { IssueBlock } from "./block"; import { KanbanIssueBlocksList } from "components/issues";
// types
import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
// mobx
import { observer } from "mobx-react-lite";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
export interface IGroupByKanBan { export interface IGroupByKanBan {
issues: any; issues: any;
@ -20,14 +19,20 @@ export interface IGroupByKanBan {
list: any; list: any;
listKey: string; listKey: string;
isDragDisabled: boolean; isDragDisabled: boolean;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; handleIssues: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
} }
const GroupByKanBan: React.FC<IGroupByKanBan> = observer( const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
({ const {
issues, issues,
sub_group_by, sub_group_by,
group_by, group_by,
@ -36,74 +41,76 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
listKey, listKey,
isDragDisabled, isDragDisabled,
handleIssues, handleIssues,
quickActions,
display_properties, display_properties,
kanBanToggle, kanBanToggle,
handleKanBanToggle, handleKanBanToggle,
}) => { } = props;
const verticalAlignPosition = (_list: any) =>
kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
return ( const verticalAlignPosition = (_list: any) =>
<div className="relative w-full h-full flex"> kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
{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 return (
className={`min-h-[150px] h-full ${ <div className="relative w-full h-full flex">
verticalAlignPosition(_list) ? `w-[0px] overflow-hidden` : `w-full transition-all` {list &&
}`} list.length > 0 &&
> list.map((_list: any) => (
<Droppable droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}> <div className={`flex-shrink-0 flex flex-col ${!verticalAlignPosition(_list) ? `w-[340px]` : ``}`}>
{(provided: any, snapshot: any) => ( {sub_group_by === null && (
<div <div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]">
className={`w-full h-full relative transition-all ${ <KanBanGroupByHeaderRoot
snapshot.isDraggingOver ? `bg-custom-background-80` : `` column_id={getValueFromObject(_list, listKey) as string}
}`} column_value={_list}
{...provided.droppableProps} sub_group_by={sub_group_by}
ref={provided.innerRef} group_by={group_by}
> issues_count={issues?.[getValueFromObject(_list, listKey) as string]?.length || 0}
{issues ? ( kanBanToggle={kanBanToggle}
<IssueBlock handleKanBanToggle={handleKanBanToggle}
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>
</div> </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> ))}
); </div>
} );
); });
export interface IKanBan { export interface IKanBan {
issues: any; issues: any;
@ -111,7 +118,13 @@ export interface IKanBan {
group_by: string | null; group_by: string | null;
sub_group_id?: string; sub_group_id?: string;
handleDragDrop?: (result: any) => void | undefined; handleDragDrop?: (result: any) => void | undefined;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; handleIssues: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
@ -132,6 +145,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
group_by, group_by,
sub_group_id = "null", sub_group_id = "null",
handleIssues, handleIssues,
quickActions,
display_properties, display_properties,
kanBanToggle, kanBanToggle,
handleKanBanToggle, handleKanBanToggle,
@ -144,7 +158,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
estimates, estimates,
} = props; } = props;
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); const { project: projectStore, issueKanBanView: issueKanBanViewStore } = useMobxStore();
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
@ -158,6 +172,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
listKey={`id`} listKey={`id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -174,6 +189,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
listKey={`key`} listKey={`key`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -190,6 +206,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
listKey={`key`} listKey={`key`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -206,6 +223,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
listKey={`id`} listKey={`id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -222,6 +240,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
listKey={`member.id`} listKey={`member.id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -238,6 +257,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
listKey={`member.id`} listKey={`member.id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}

View File

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

View File

@ -1,14 +1,15 @@
import React from "react"; import React, { useCallback } from "react";
// react beautiful dnd import { useRouter } from "next/router";
import { DragDropContext } from "@hello-pangea/dnd";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanSwimLanes } from "./swimlanes"; import { KanBanSwimLanes } from "./swimlanes";
import { KanBan } from "./default"; import { KanBan } from "./default";
// store import { ModuleIssueQuickActions } from "components/issues";
import { useMobxStore } from "lib/mobx/store-provider"; // types
import { RootStore } from "store/root"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
@ -20,7 +21,11 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
moduleIssue: moduleIssueStore, moduleIssue: moduleIssueStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
moduleIssueKanBanView: moduleIssueKanBanViewStore, moduleIssueKanBanView: moduleIssueKanBanViewStore,
}: RootStore = useMobxStore(); issueDetail: issueDetailStore,
} = useMobxStore();
const router = useRouter();
const { workspaceSlug, moduleId } = router.query;
const issues = moduleIssueStore?.getIssues; const issues = moduleIssueStore?.getIssues;
@ -50,9 +55,27 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
: moduleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); : moduleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
}; };
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { const handleIssues = useCallback(
moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); (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) => { const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value); moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
@ -74,7 +97,15 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -91,7 +122,15 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}

View File

@ -1,15 +1,17 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { DragDropContext } from "@hello-pangea/dnd"; import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanSwimLanes } from "./swimlanes"; import { KanBanSwimLanes } from "./swimlanes";
import { KanBan } from "./default"; import { KanBan } from "./default";
// store import { ProjectIssueQuickActions } from "components/issues";
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
// types
import { IIssue } from "types";
export interface IProfileIssuesKanBanLayout {} export interface IProfileIssuesKanBanLayout {}
@ -20,7 +22,11 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
profileIssues: profileIssuesStore, profileIssues: profileIssuesStore,
profileIssueFilters: profileIssueFiltersStore, profileIssueFilters: profileIssueFiltersStore,
issueKanBanView: issueKanBanViewStore, issueKanBanView: issueKanBanViewStore,
}: RootStore = useMobxStore(); issueDetail: issueDetailStore,
} = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
const issues = profileIssuesStore?.getIssues; const issues = profileIssuesStore?.getIssues;
@ -50,9 +56,18 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
}; };
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { const handleIssues = useCallback(
profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue); (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) => { const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
issueKanBanViewStore.handleKanBanToggle(toggle, value); issueKanBanViewStore.handleKanBanToggle(toggle, value);
@ -74,7 +89,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle} kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -91,7 +113,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle} kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}

View File

@ -1,24 +1,31 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { useRouter } from "next/router";
import { DragDropContext } from "@hello-pangea/dnd"; import { DragDropContext } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanSwimLanes } from "./swimlanes"; import { KanBanSwimLanes } from "./swimlanes";
import { KanBan } from "./default"; import { KanBan } from "./default";
// store import { ProjectIssueQuickActions } from "components/issues";
import { useMobxStore } from "lib/mobx/store-provider"; // types
import { RootStore } from "store/root"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
export interface IKanBanLayout {} export interface IKanBanLayout {}
export const KanBanLayout: FC = observer(() => { export const KanBanLayout: FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { const {
project: projectStore, project: projectStore,
issue: issueStore, issue: issueStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
issueKanBanView: issueKanBanViewStore, issueKanBanView: issueKanBanViewStore,
}: RootStore = useMobxStore(); issueDetail: issueDetailStore,
} = useMobxStore();
const issues = issueStore?.getIssues; const issues = issueStore?.getIssues;
@ -48,9 +55,18 @@ export const KanBanLayout: FC = observer(() => {
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
}; };
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { const handleIssues = useCallback(
issueStore.updateIssueStructure(group_by, sub_group_by, issue); (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) => { const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
issueKanBanViewStore.handleKanBanToggle(toggle, value); issueKanBanViewStore.handleKanBanToggle(toggle, value);
@ -72,7 +88,14 @@ export const KanBanLayout: FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle} kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -89,7 +112,14 @@ export const KanBanLayout: FC = observer(() => {
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle} kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}

View File

@ -1,15 +1,15 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
import { KanBan } from "./default"; import { KanBan } from "./default";
// types
import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
// mobx
import { observer } from "mobx-react-lite";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
interface ISubGroupSwimlaneHeader { interface ISubGroupSwimlaneHeader {
issues: any; issues: any;
@ -61,7 +61,13 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issues: any; issues: any;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; handleIssues: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
@ -81,6 +87,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
list, list,
listKey, listKey,
handleIssues, handleIssues,
quickActions,
display_properties, display_properties,
kanBanToggle, kanBanToggle,
handleKanBanToggle, handleKanBanToggle,
@ -130,6 +137,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
group_by={group_by} group_by={group_by}
sub_group_id={getValueFromObject(_list, listKey) as string} sub_group_id={getValueFromObject(_list, listKey) as string}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -153,7 +161,13 @@ export interface IKanBanSwimLanes {
issues: any; issues: any;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; handleIssues: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
@ -172,6 +186,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
sub_group_by, sub_group_by,
group_by, group_by,
handleIssues, handleIssues,
quickActions,
display_properties, display_properties,
kanBanToggle, kanBanToggle,
handleKanBanToggle, handleKanBanToggle,
@ -184,7 +199,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
estimates, estimates,
} = props; } = props;
const { project: projectStore }: RootStore = useMobxStore(); const { project: projectStore } = useMobxStore();
return ( return (
<div className="relative"> <div className="relative">
@ -270,6 +285,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
list={projectStore?.projectStates} list={projectStore?.projectStates}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -291,6 +307,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
list={ISSUE_STATE_GROUPS} list={ISSUE_STATE_GROUPS}
listKey={`key`} listKey={`key`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -312,6 +329,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
list={ISSUE_PRIORITIES} list={ISSUE_PRIORITIES}
listKey={`key`} listKey={`key`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -333,6 +351,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
list={projectStore?.projectLabels} list={projectStore?.projectLabels}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -354,6 +373,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
list={projectStore?.projectMembers} list={projectStore?.projectMembers}
listKey={`member.id`} listKey={`member.id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
@ -375,6 +395,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
list={projectStore?.projectMembers} list={projectStore?.projectMembers}
listKey={`member.id`} listKey={`member.id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}

View File

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

View File

@ -1,14 +1,16 @@
import { FC } from "react";
// components // components
import { KanBanProperties } from "./properties"; import { KanBanProperties } from "./properties";
import { IssuePeekOverview } from "components/issues/issue-peek-overview"; import { IssuePeekOverview } from "components/issues/issue-peek-overview";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types
import { IIssue } from "types";
interface IssueBlockProps { interface IssueBlockProps {
columnId: string; columnId: string;
issues: any; issue: IIssue;
handleIssues?: (group_by: string | null, issue: any) => void; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
states: any; states: any;
labels: any; labels: any;
@ -16,53 +18,48 @@ interface IssueBlockProps {
priorities: any; priorities: any;
} }
export const IssueBlock: FC<IssueBlockProps> = (props) => { export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
const { columnId, issues, handleIssues, display_properties, states, labels, members, priorities } = props; const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, priorities } =
props;
const handleIssue = (_issue: any) => { const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
if (_issue && handleIssues) handleIssues(!columnId && columnId === "null" ? null : columnId, _issue); if (issueToUpdate && handleIssues) handleIssues(group_by, issueToUpdate, "update");
}; };
return ( return (
<> <>
{issues && <div className="text-sm p-3 shadow-custom-shadow-2xs bg-custom-background-100 flex items-center gap-3 border-b border-custom-border-200 hover:bg-custom-background-80">
issues?.length > 0 && {display_properties && display_properties?.key && (
issues.map((issue: any, index: any) => ( <div className="flex-shrink-0 text-xs text-custom-text-300">
<div {issue?.project_detail?.identifier}-{issue.sequence_id}
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> </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>
</> </>
); );
}; };

View File

@ -0,0 +1,43 @@
import { FC } from "react";
// components
import { IssueBlock } from "components/issues";
// types
import { IIssue } from "types";
interface Props {
columnId: string;
issues: IIssue[];
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
states: any;
labels: any;
members: any;
priorities: any;
}
export const IssueBlocksList: FC<Props> = (props) => {
const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, priorities } =
props;
return (
<>
{issues &&
issues?.length > 0 &&
issues.map((issue) => (
<IssueBlock
key={issue.id}
columnId={columnId}
issue={issue}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
priorities={priorities}
/>
))}
</>
);
};

View File

@ -1,21 +1,28 @@
import React from "react"; import React, { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "./default"; import { List } from "./default";
// store import { CycleIssueQuickActions } from "components/issues";
import { useMobxStore } from "lib/mobx/store-provider"; // types
import { RootStore } from "store/root"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
export interface ICycleListLayout {} export interface ICycleListLayout {}
export const CycleListLayout: React.FC = observer(() => { export const CycleListLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, cycleId } = router.query;
const { const {
project: projectStore, project: projectStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
cycleIssue: cycleIssueStore, cycleIssue: cycleIssueStore,
}: RootStore = useMobxStore(); issueDetail: issueDetailStore,
} = useMobxStore();
const issues = cycleIssueStore?.getIssues; const issues = cycleIssueStore?.getIssues;
@ -23,9 +30,27 @@ export const CycleListLayout: React.FC = observer(() => {
const display_properties = issueFilterStore?.userDisplayProperties || null; const display_properties = issueFilterStore?.userDisplayProperties || null;
const updateIssue = (group_by: string | null, issue: any) => { const handleIssues = useCallback(
cycleIssueStore.updateIssueStructure(group_by, null, issue); (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 states = projectStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
@ -40,7 +65,15 @@ export const CycleListLayout: React.FC = observer(() => {
<List <List
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
handleRemoveFromCycle={async () => handleIssues(group_by, issue, "remove")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}

View File

@ -5,13 +5,16 @@ import { ListGroupByHeaderRoot } from "./headers/group-by-root";
import { IssueBlock } from "./block"; import { IssueBlock } from "./block";
// constants // constants
import { getValueFromObject } from "constants/issue"; import { getValueFromObject } from "constants/issue";
import { IIssue } from "types";
import { IssueBlocksList } from "./blocks-list";
export interface IGroupByList { export interface IGroupByList {
issues: any; issues: any;
group_by: string | null; group_by: string | null;
list: any; list: any;
listKey: string; listKey: string;
handleIssues?: (group_by: string | null, issue: any) => void; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
is_list?: boolean; is_list?: boolean;
states: any; states: any;
@ -30,6 +33,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
list, list,
listKey, listKey,
handleIssues, handleIssues,
quickActions,
display_properties, display_properties,
is_list = false, is_list = false,
states, states,
@ -59,10 +63,11 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
</div> </div>
<div className={`w-full h-full relative transition-all`}> <div className={`w-full h-full relative transition-all`}>
{issues && ( {issues && (
<IssueBlock <IssueBlocksList
columnId={getValueFromObject(_list, listKey) as string} columnId={getValueFromObject(_list, listKey) as string}
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]} issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}
@ -77,11 +82,13 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
); );
}); });
// TODO: update all the types
export interface IList { export interface IList {
issues: any; issues: any;
group_by: string | null; group_by: string | null;
handleDragDrop?: (result: any) => void | undefined; handleDragDrop?: (result: any) => void | undefined;
handleIssues?: (group_by: string | null, issue: any) => void; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
states: any; states: any;
labels: any; labels: any;
@ -97,6 +104,7 @@ export const List: React.FC<IList> = observer((props) => {
issues, issues,
group_by, group_by,
handleIssues, handleIssues,
quickActions,
display_properties, display_properties,
states, states,
labels, labels,
@ -116,6 +124,7 @@ export const List: React.FC<IList> = observer((props) => {
list={[{ id: "null", title: "All Issues" }]} list={[{ id: "null", title: "All Issues" }]}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
is_list={true} is_list={true}
states={states} states={states}
@ -135,6 +144,7 @@ export const List: React.FC<IList> = observer((props) => {
list={projects} list={projects}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}
@ -153,6 +163,7 @@ export const List: React.FC<IList> = observer((props) => {
list={states} list={states}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}
@ -171,6 +182,7 @@ export const List: React.FC<IList> = observer((props) => {
list={stateGroups} list={stateGroups}
listKey={`key`} listKey={`key`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}
@ -189,6 +201,7 @@ export const List: React.FC<IList> = observer((props) => {
list={priorities} list={priorities}
listKey={`key`} listKey={`key`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}
@ -207,6 +220,7 @@ export const List: React.FC<IList> = observer((props) => {
list={labels} list={labels}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}
@ -225,6 +239,7 @@ export const List: React.FC<IList> = observer((props) => {
list={members} list={members}
listKey={`member.id`} listKey={`member.id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}
@ -243,6 +258,7 @@ export const List: React.FC<IList> = observer((props) => {
list={members} list={members}
listKey={`member.id`} listKey={`member.id`}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
labels={labels} labels={labels}

View File

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

View File

@ -1,21 +1,28 @@
import React from "react"; import React, { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "./default"; import { List } from "./default";
// store import { ModuleIssueQuickActions } from "components/issues";
import { useMobxStore } from "lib/mobx/store-provider"; // types
import { RootStore } from "store/root"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
export interface IModuleListLayout {} export interface IModuleListLayout {}
export const ModuleListLayout: React.FC = observer(() => { export const ModuleListLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, moduleId } = router.query;
const { const {
project: projectStore, project: projectStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
moduleIssue: moduleIssueStore, moduleIssue: moduleIssueStore,
}: RootStore = useMobxStore(); issueDetail: issueDetailStore,
} = useMobxStore();
const issues = moduleIssueStore?.getIssues; const issues = moduleIssueStore?.getIssues;
@ -23,9 +30,27 @@ export const ModuleListLayout: React.FC = observer(() => {
const display_properties = issueFilterStore?.userDisplayProperties || null; const display_properties = issueFilterStore?.userDisplayProperties || null;
const updateIssue = (group_by: string | null, issue: any) => { const handleIssues = useCallback(
moduleIssueStore.updateIssueStructure(group_by, null, issue); (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 states = projectStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
@ -40,7 +65,15 @@ export const ModuleListLayout: React.FC = observer(() => {
<List <List
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
handleRemoveFromModule={async () => handleIssues(group_by, issue, "remove")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}

View File

@ -1,10 +1,13 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "./default"; import { List } from "./default";
// store import { ProjectIssueQuickActions } from "components/issues";
import { useMobxStore } from "lib/mobx/store-provider"; // types
import { RootStore } from "store/root"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
@ -15,18 +18,31 @@ export const ProfileIssuesListLayout: FC = observer(() => {
workspace: workspaceStore, workspace: workspaceStore,
project: projectStore, project: projectStore,
profileIssueFilters: profileIssueFiltersStore, profileIssueFilters: profileIssueFiltersStore,
profileIssues: profileIssuesIssueStore, profileIssues: profileIssuesStore,
}: RootStore = useMobxStore(); issueDetail: issueDetailStore,
} = useMobxStore();
const issues = profileIssuesIssueStore?.getIssues; const router = useRouter();
const { workspaceSlug } = router.query;
const issues = profileIssuesStore?.getIssues;
const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null;
const display_properties = profileIssueFiltersStore?.userDisplayProperties || null; const display_properties = profileIssueFiltersStore?.userDisplayProperties || null;
const updateIssue = (group_by: string | null, issue: any) => { const handleIssues = useCallback(
profileIssuesIssueStore.updateIssueStructure(group_by, null, issue); (group_by: string | null, issue: IIssue, action: "update" | "delete") => {
}; if (!workspaceSlug) return;
if (action === "update") {
profileIssuesStore.updateIssueStructure(group_by, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
}
if (action === "delete") profileIssuesStore.deleteIssue(group_by, null, issue);
},
[profileIssuesStore, issueDetailStore, workspaceSlug]
);
const states = projectStore?.projectStates || null; const states = projectStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
@ -41,7 +57,14 @@ export const ProfileIssuesListLayout: FC = observer(() => {
<List <List
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}

View File

@ -10,11 +10,13 @@ import { IssuePropertyEstimates } from "../properties/estimates";
import { IssuePropertyDate } from "../properties/date"; import { IssuePropertyDate } from "../properties/date";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types
import { IIssue } from "types";
export interface IKanBanProperties { export interface IKanBanProperties {
columnId: string; columnId: string;
issue: any; issue: any;
handleIssues?: (group_by: string | null, issue: any) => void; handleIssues?: (group_by: string | null, issue: IIssue) => void;
display_properties: any; display_properties: any;
states: any; states: any;
labels: any; labels: any;

View File

@ -1,16 +1,26 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components
import { List } from "./default";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components
import { List } from "./default";
import { ProjectIssueQuickActions } from "components/issues";
// types // types
import { RootStore } from "store/root"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
export const ListLayout: FC = observer(() => { export const ListLayout: FC = observer(() => {
const { project: projectStore, issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); const router = useRouter();
const { workspaceSlug } = router.query;
const {
project: projectStore,
issue: issueStore,
issueDetail: issueDetailStore,
issueFilter: issueFilterStore,
} = useMobxStore();
const issues = issueStore?.getIssues; const issues = issueStore?.getIssues;
@ -18,9 +28,18 @@ export const ListLayout: FC = observer(() => {
const display_properties = issueFilterStore?.userDisplayProperties || null; const display_properties = issueFilterStore?.userDisplayProperties || null;
const updateIssue = (group_by: string | null, issue: any) => { const handleIssues = useCallback(
issueStore.updateIssueStructure(group_by, null, issue); (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 states = projectStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
@ -31,11 +50,18 @@ export const ListLayout: FC = observer(() => {
const estimates = null; const estimates = null;
return ( return (
<div className={`relative w-full h-full bg-custom-background-90`}> <div className="relative w-full h-full bg-custom-background-90">
<List <List
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
handleIssues={updateIssue} handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
/>
)}
display_properties={display_properties} display_properties={display_properties}
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}

View File

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

View File

@ -0,0 +1,130 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { CustomMenu } from "@plane/ui";
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// components
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { IIssue } from "types";
type Props = {
issue: IIssue;
handleDelete: () => Promise<void>;
handleUpdate: (data: IIssue) => Promise<void>;
handleRemoveFromCycle: () => Promise<void>;
};
export const CycleIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromCycle } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const { setToastAlert } = useToast();
const handleCopyIssueLink = () => {
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
setToastAlert({
type: "success",
title: "Link copied",
message: "Issue link copied to clipboard",
})
);
};
return (
<>
<DeleteIssueModal
data={issue}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete}
/>
<CreateUpdateIssueModal
isOpen={createUpdateIssueModal}
handleClose={() => {
setCreateUpdateIssueModal(false);
setIssueToEdit(null);
}}
// pre-populate date only if not editing
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
data={issueToEdit}
onSubmit={async (data) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
Copy link
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
Edit issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromCycle();
}}
>
<div className="flex items-center gap-2">
<XCircle className="h-3 w-3" />
Remove from cycle
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Make a copy
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<Trash2 className="h-3 w-3" />
Delete issue
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</>
);
};

View File

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

View File

@ -0,0 +1,130 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { CustomMenu } from "@plane/ui";
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// components
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { IIssue } from "types";
type Props = {
issue: IIssue;
handleDelete: () => Promise<void>;
handleUpdate: (data: IIssue) => Promise<void>;
handleRemoveFromModule: () => Promise<void>;
};
export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromModule } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const { setToastAlert } = useToast();
const handleCopyIssueLink = () => {
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
setToastAlert({
type: "success",
title: "Link copied",
message: "Issue link copied to clipboard",
})
);
};
return (
<>
<DeleteIssueModal
data={issue}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete}
/>
<CreateUpdateIssueModal
isOpen={createUpdateIssueModal}
handleClose={() => {
setCreateUpdateIssueModal(false);
setIssueToEdit(null);
}}
// pre-populate date only if not editing
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
data={issueToEdit}
onSubmit={async (data) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
Copy link
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
Edit issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromModule();
}}
>
<div className="flex items-center gap-2">
<XCircle className="h-3 w-3" />
Remove from module
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Make a copy
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<Trash2 className="h-3 w-3" />
Delete issue
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</>
);
};

View File

@ -0,0 +1,117 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { CustomMenu } from "@plane/ui";
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// components
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { IIssue } from "types";
type Props = {
issue: IIssue;
handleDelete: () => Promise<void>;
handleUpdate: (data: IIssue) => Promise<void>;
};
export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const { setToastAlert } = useToast();
const handleCopyIssueLink = () => {
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() =>
setToastAlert({
type: "success",
title: "Link copied",
message: "Issue link copied to clipboard",
})
);
};
return (
<>
<DeleteIssueModal
data={issue}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete}
/>
<CreateUpdateIssueModal
isOpen={createUpdateIssueModal}
handleClose={() => {
setCreateUpdateIssueModal(false);
setIssueToEdit(null);
}}
// pre-populate date only if not editing
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
data={issueToEdit}
onSubmit={async (data) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCopyIssueLink();
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
Copy link
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
Edit issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Make a copy
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2 text-red-500">
<Trash2 className="h-3 w-3" />
Delete issue
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</>
);
};

View File

@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Popover2 } from "@blueprintjs/popover2"; import { Popover2 } from "@blueprintjs/popover2";
import { MoreHorizontal, LinkIcon, Pencil, Trash2, ChevronRight } from "lucide-react"; import { MoreHorizontal, Pencil, Trash2, ChevronRight, Link } from "lucide-react";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// helpers // helpers
@ -82,6 +82,20 @@ export const IssueColumn: React.FC<Props> = ({
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)} onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={ content={
<div className="flex flex-col whitespace-nowrap rounded-md border border-custom-border-100 p-1 text-xs shadow-lg focus:outline-none min-w-full bg-custom-background-100 space-y-0.5"> <div className="flex flex-col whitespace-nowrap rounded-md border border-custom-border-100 p-1 text-xs shadow-lg focus:outline-none min-w-full bg-custom-background-100 space-y-0.5">
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center gap-2">
<Link className="h-3 w-3" />
<span>Copy link</span>
</div>
</button>
<button <button
type="button" type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80" className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
@ -98,7 +112,7 @@ export const IssueColumn: React.FC<Props> = ({
<button <button
type="button" type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80" className="w-full select-none gap-2 rounded p-1 text-left text-red-500 hover:bg-custom-background-80"
onClick={() => { onClick={() => {
handleDeleteIssue(issue); handleDeleteIssue(issue);
setIsOpen(false); setIsOpen(false);
@ -109,20 +123,6 @@ export const IssueColumn: React.FC<Props> = ({
<span>Delete issue</span> <span>Delete issue</span>
</div> </div>
</button> </button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 rounded p-1 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy issue link</span>
</div>
</button>
</div> </div>
} }
placement="bottom-start" placement="bottom-start"

View File

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

View File

@ -20,10 +20,8 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
cycleIssue: cycleIssueStore, cycleIssue: cycleIssueStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, project: projectStore,
user: userStore,
} = useMobxStore(); } = useMobxStore();
const user = userStore.currentUser;
const issues = cycleIssueStore.getIssues; const issues = cycleIssueStore.getIssues;
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFiltersUpdate = useCallback(
@ -41,7 +39,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
const handleUpdateIssue = useCallback( const handleUpdateIssue = useCallback(
(issue: IIssue, data: Partial<IIssue>) => { (issue: IIssue, data: Partial<IIssue>) => {
if (!workspaceSlug || !projectId || !user) return; if (!workspaceSlug || !projectId) return;
const payload = { const payload = {
...issue, ...issue,
@ -49,9 +47,9 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
}; };
cycleIssueStore.updateIssueStructure(null, null, payload); cycleIssueStore.updateIssueStructure(null, null, payload);
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
}, },
[issueDetailStore, cycleIssueStore, projectId, user, workspaceSlug] [issueDetailStore, cycleIssueStore, projectId, workspaceSlug]
); );
return ( return (

View File

@ -20,10 +20,8 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
moduleIssue: moduleIssueStore, moduleIssue: moduleIssueStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, project: projectStore,
user: userStore,
} = useMobxStore(); } = useMobxStore();
const user = userStore.currentUser;
const issues = moduleIssueStore.getIssues; const issues = moduleIssueStore.getIssues;
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFiltersUpdate = useCallback(
@ -41,7 +39,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
const handleUpdateIssue = useCallback( const handleUpdateIssue = useCallback(
(issue: IIssue, data: Partial<IIssue>) => { (issue: IIssue, data: Partial<IIssue>) => {
if (!workspaceSlug || !projectId || !user) return; if (!workspaceSlug || !projectId) return;
const payload = { const payload = {
...issue, ...issue,
@ -49,9 +47,9 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
}; };
moduleIssueStore.updateIssueStructure(null, null, payload); moduleIssueStore.updateIssueStructure(null, null, payload);
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
}, },
[issueDetailStore, moduleIssueStore, projectId, user, workspaceSlug] [issueDetailStore, moduleIssueStore, projectId, workspaceSlug]
); );
return ( return (

View File

@ -49,7 +49,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => {
}; };
issueStore.updateIssueStructure(null, null, payload); issueStore.updateIssueStructure(null, null, payload);
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
}, },
[issueStore, issueDetailStore, projectId, user, workspaceSlug] [issueStore, issueDetailStore, projectId, user, workspaceSlug]
); );

View File

@ -20,10 +20,8 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
projectViewIssues: projectViewIssueStore, projectViewIssues: projectViewIssueStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, project: projectStore,
user: userStore,
} = useMobxStore(); } = useMobxStore();
const user = userStore.currentUser;
const issues = projectViewIssueStore.getIssues; const issues = projectViewIssueStore.getIssues;
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFiltersUpdate = useCallback(
@ -41,7 +39,7 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
const handleUpdateIssue = useCallback( const handleUpdateIssue = useCallback(
(issue: IIssue, data: Partial<IIssue>) => { (issue: IIssue, data: Partial<IIssue>) => {
if (!workspaceSlug || !projectId || !user) return; if (!workspaceSlug || !projectId) return;
const payload = { const payload = {
...issue, ...issue,
@ -49,9 +47,9 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
}; };
projectViewIssueStore.updateIssueStructure(null, null, payload); projectViewIssueStore.updateIssueStructure(null, null, payload);
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data, user); issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
}, },
[issueDetailStore, projectViewIssueStore, projectId, user, workspaceSlug] [issueDetailStore, projectViewIssueStore, projectId, workspaceSlug]
); );
return ( return (

View File

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

View File

@ -16,13 +16,12 @@ import { IssueForm, ConfirmIssueDiscard } from "components/issues";
// types // types
import type { IIssue } from "types"; import type { IIssue } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUES_DETAILS, USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys"; import { USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys";
export interface IssuesModalProps { export interface IssuesModalProps {
data?: IIssue | null; data?: IIssue | null;
handleClose: () => void; handleClose: () => void;
isOpen: boolean; isOpen: boolean;
isUpdatingSingleIssue?: boolean;
prePopulateData?: Partial<IIssue>; prePopulateData?: Partial<IIssue>;
fieldsToShow?: ( fieldsToShow?: (
| "project" | "project"
@ -46,15 +45,7 @@ const issueService = new IssueService();
const issueDraftService = new IssueDraftService(); const issueDraftService = new IssueDraftService();
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => { export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
const { const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props;
data,
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData: prePopulateDataProps,
fieldsToShow = ["all"],
onSubmit,
} = props;
// states // states
const [createMore, setCreateMore] = useState(false); const [createMore, setCreateMore] = useState(false);
@ -211,10 +202,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}; };
const createIssue = async (payload: Partial<IIssue>) => { const createIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return; if (!workspaceSlug || !activeProject) return;
await issueDetailStore await issueDetailStore
.createIssue(workspaceSlug.toString(), activeProject, payload, user) .createIssue(workspaceSlug.toString(), activeProject, payload)
.then(async (res) => { .then(async (res) => {
issueStore.fetchIssues(workspaceSlug.toString(), activeProject); issueStore.fetchIssues(workspaceSlug.toString(), activeProject);
@ -280,16 +271,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
await issueService await issueService
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user) .patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => { .then(() => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
}
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) onFormSubmitClose(); if (!createMore) onFormSubmitClose();
setToastAlert({ setToastAlert({

View File

@ -6,7 +6,6 @@ import { IssueLabelService } from "services/issue";
// hooks // hooks
import useMyIssues from "hooks/my-issues/use-my-issues"; import useMyIssues from "hooks/my-issues/use-my-issues";
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import useUserAuth from "hooks/use-user-auth";
// components // components
import { FiltersList } from "components/core"; import { FiltersList } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
@ -43,8 +42,6 @@ export const MyIssuesView: React.FC<Props> = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { user } = useUserAuth();
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString()); const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
@ -220,15 +217,16 @@ export const MyIssuesView: React.FC<Props> = () => {
mutateMyIssues(); mutateMyIssues();
}} }}
/> />
<DeleteIssueModal {issueToDelete && (
handleClose={() => setDeleteIssueModal(false)} <DeleteIssueModal
isOpen={deleteIssueModal} handleClose={() => setDeleteIssueModal(false)}
data={issueToDelete} isOpen={deleteIssueModal}
user={user} data={issueToDelete}
onSubmit={async () => { onSubmit={async () => {
mutateMyIssues(); mutateMyIssues();
}} }}
/> />
)}
{areFiltersApplied && ( {areFiltersApplied && (
<> <>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0"> <div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">

View File

@ -8,8 +8,6 @@ import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// hooks
import useUser from "hooks/use-user";
// components // components
import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues"; import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues";
// types // types
@ -40,8 +38,6 @@ export const IssuePeekOverview: React.FC<Props> = observer(({ handleMutation, pr
const issue = issues[peekIssue?.toString() ?? ""]; const issue = issues[peekIssue?.toString() ?? ""];
const { user } = useUser();
const handleClose = () => { const handleClose = () => {
const { query } = router; const { query } = router;
delete query.peekIssue; delete query.peekIssue;
@ -53,17 +49,17 @@ export const IssuePeekOverview: React.FC<Props> = observer(({ handleMutation, pr
}; };
const handleUpdateIssue = async (formData: Partial<IIssue>) => { const handleUpdateIssue = async (formData: Partial<IIssue>) => {
if (!issue || !user) return; if (!issue) return;
await updateIssue(workspaceSlug, projectId, issue.id, formData, user); await updateIssue(workspaceSlug, projectId, issue.id, formData);
mutate(PROJECT_ISSUES_ACTIVITY(issue.id)); mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
if (handleMutation) handleMutation(); if (handleMutation) handleMutation();
}; };
const handleDeleteIssue = async () => { const handleDeleteIssue = async () => {
if (!issue || !user) return; if (!issue) return;
await deleteIssue(workspaceSlug, projectId, issue.id, user); await deleteIssue(workspaceSlug, projectId, issue.id);
if (handleMutation) handleMutation(); if (handleMutation) handleMutation();
handleClose(); handleClose();
@ -92,13 +88,14 @@ export const IssuePeekOverview: React.FC<Props> = observer(({ handleMutation, pr
return ( return (
<> <>
<DeleteIssueModal {issue && (
isOpen={deleteIssueModal} <DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal}
data={issue ? { ...issue } : null} handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDeleteIssue} data={issue}
user={user} onSubmit={handleDeleteIssue}
/> />
)}
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}> <Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <Dialog as="div" className="relative z-20" onClose={handleClose}>
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto"> <div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">

View File

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

View File

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

View File

@ -1,9 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr"; import { mutate } from "swr";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react"; import { Disclosure, Popover, Transition } from "@headlessui/react";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import { ModuleService } from "services/module.service"; import { ModuleService } from "services/module.service";
// contexts // contexts
@ -14,6 +17,7 @@ import useToast from "hooks/use-toast";
import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules"; import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules";
import ProgressChart from "components/core/sidebar/progress-chart"; import ProgressChart from "components/core/sidebar/progress-chart";
// ui
import { CustomSelect, CustomMenu, Loader, ProgressBar } from "@plane/ui"; import { CustomSelect, CustomMenu, Loader, ProgressBar } from "@plane/ui";
// icon // icon
import { import {
@ -29,9 +33,9 @@ import {
} from "lucide-react"; } from "lucide-react";
// helpers // helpers
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IUser, IIssue, linkDetails, IModule, ModuleLink } from "types"; import { linkDetails, IModule, ModuleLink } from "types";
// fetch-keys // fetch-keys
import { MODULE_DETAILS } from "constants/fetch-keys"; import { MODULE_DETAILS } from "constants/fetch-keys";
// constant // constant
@ -46,21 +50,28 @@ const defaultValues: Partial<IModule> = {
}; };
type Props = { type Props = {
module?: IModule;
isOpen: boolean; isOpen: boolean;
moduleIssues?: IIssue[]; moduleId: string;
user: IUser | undefined;
}; };
// services
const moduleService = new ModuleService(); const moduleService = new ModuleService();
export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIssues, user }) => { // TODO: refactor this component
export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const { isOpen, moduleId } = props;
const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId } = router.query;
const { module: moduleStore, user: userStore } = useMobxStore();
const user = userStore.currentUser ?? undefined;
const moduleDetails = moduleStore.moduleDetails[moduleId] ?? undefined;
const { memberRole } = useProjectMyMembership(); const { memberRole } = useProjectMyMembership();
@ -117,7 +128,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
const payload = { metadata: {}, ...formData }; const payload = { metadata: {}, ...formData };
const updatedLinks = module.link_module.map((l) => const updatedLinks = moduleDetails.link_module.map((l) =>
l.id === linkId l.id === linkId
? { ? {
...l, ...l,
@ -146,7 +157,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
const handleDeleteLink = async (linkId: string) => { const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !module) return; if (!workspaceSlug || !projectId || !module) return;
const updatedLinks = module.link_module.filter((l) => l.id !== linkId); const updatedLinks = moduleDetails.link_module.filter((l) => l.id !== linkId);
mutate<IModule>( mutate<IModule>(
MODULE_DETAILS(module.id), MODULE_DETAILS(module.id),
@ -165,41 +176,45 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
}; };
const handleCopyText = () => { const handleCopyText = () => {
// const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`)
copyTextToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Module link copied to clipboard", title: "Link copied",
message: "Module link copied to clipboard",
}); });
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Some error occurred", title: "Error!",
message: "Some error occurred",
}); });
}); });
}; };
useEffect(() => { useEffect(() => {
if (module) if (moduleDetails)
reset({ reset({
...module, ...moduleDetails,
members_list: module.members_list ?? module.members_detail?.map((m) => m.id), members_list: moduleDetails.members_list ?? moduleDetails.members_detail?.map((m) => m.id),
}); });
}, [module, reset]); }, [moduleDetails, reset]);
const isStartValid = new Date(`${module?.start_date}`) <= new Date(); const isStartValid = new Date(`${moduleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${module?.target_date}`) >= new Date(`${module?.start_date}`); const isEndValid = new Date(`${moduleDetails?.target_date}`) >= new Date(`${moduleDetails?.start_date}`);
const progressPercentage = module ? Math.round((module.completed_issues / module.total_issues) * 100) : null; const progressPercentage = moduleDetails
? Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100)
: null;
const handleEditLink = (link: linkDetails) => { const handleEditLink = (link: linkDetails) => {
setSelectedLinkToUpdate(link); setSelectedLinkToUpdate(link);
setModuleLinkModal(true); setModuleLinkModal(true);
}; };
if (!moduleDetails) return null;
return ( return (
<> <>
<LinkModal <LinkModal
@ -213,7 +228,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
createIssueLink={handleCreateLink} createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink} updateIssueLink={handleUpdateLink}
/> />
<DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} user={user} /> <DeleteModuleModal isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={moduleDetails} user={user} />
<div <div
className={`fixed top-[66px] ${ className={`fixed top-[66px] ${
isOpen ? "right-0" : "-right-[24rem]" isOpen ? "right-0" : "-right-[24rem]"
@ -254,11 +269,13 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<> <>
<Popover.Button <Popover.Button
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${ className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
module.start_date ? "" : "text-custom-text-200" moduleDetails.start_date ? "" : "text-custom-text-200"
}`} }`}
> >
<CalendarDays className="h-3 w-3" /> <CalendarDays className="h-3 w-3" />
<span>{renderShortDateWithYearFormat(new Date(`${module.start_date}`), "Start date")}</span> <span>
{renderShortDateWithYearFormat(new Date(`${moduleDetails.start_date}`), "Start date")}
</span>
</Popover.Button> </Popover.Button>
<Transition <Transition
@ -298,12 +315,14 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<> <>
<Popover.Button <Popover.Button
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${ className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
module.target_date ? "" : "text-custom-text-200" moduleDetails.target_date ? "" : "text-custom-text-200"
}`} }`}
> >
<CalendarDays className="h-3 w-3 " /> <CalendarDays className="h-3 w-3 " />
<span>{renderShortDateWithYearFormat(new Date(`${module?.target_date}`), "End date")}</span> <span>
{renderShortDateWithYearFormat(new Date(`${moduleDetails?.target_date}`), "End date")}
</span>
</Popover.Button> </Popover.Button>
<Transition <Transition
@ -342,7 +361,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<div className="flex w-full flex-col items-start justify-start gap-2"> <div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2 "> <div className="flex w-full items-start justify-between gap-2 ">
<div className="max-w-[300px]"> <div className="max-w-[300px]">
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">{module.name}</h4> <h4 className="text-xl font-semibold break-words w-full text-custom-text-100">
{moduleDetails.name}
</h4>
</div> </div>
<CustomMenu width="lg" ellipsis> <CustomMenu width="lg" ellipsis>
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}> <CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
@ -361,7 +382,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div> </div>
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full"> <span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
{module.description} {moduleDetails.description}
</span> </span>
</div> </div>
@ -399,9 +420,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<div className="flex items-center gap-2.5 text-custom-text-200"> <div className="flex items-center gap-2.5 text-custom-text-200">
<span className="h-4 w-4"> <span className="h-4 w-4">
<ProgressBar value={module.completed_issues} maxValue={module.total_issues} /> <ProgressBar value={moduleDetails.completed_issues} maxValue={moduleDetails.total_issues} />
</span> </span>
{module.completed_issues}/{module.total_issues} {moduleDetails.completed_issues}/{moduleDetails.total_issues}
</div> </div>
</div> </div>
</div> </div>
@ -415,7 +436,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<div className="flex w-full items-center justify-between gap-2 "> <div className="flex w-full items-center justify-between gap-2 ">
<div className="flex items-center justify-start gap-2 text-sm"> <div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-custom-text-200">Progress</span> <span className="font-medium text-custom-text-200">Progress</span>
{!open && moduleIssues && progressPercentage ? ( {!open && progressPercentage ? (
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]"> <span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
{progressPercentage ? `${progressPercentage}%` : ""} {progressPercentage ? `${progressPercentage}%` : ""}
</span> </span>
@ -439,7 +460,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div> </div>
<Transition show={open}> <Transition show={open}>
<Disclosure.Panel> <Disclosure.Panel>
{isStartValid && isEndValid && moduleIssues ? ( {isStartValid && isEndValid ? (
<div className=" h-full w-full py-4"> <div className=" h-full w-full py-4">
<div className="flex items-start justify-between gap-4 py-2 text-xs"> <div className="flex items-start justify-between gap-4 py-2 text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -448,7 +469,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</span> </span>
<span> <span>
Pending Issues -{" "} Pending Issues -{" "}
{module.total_issues - (module.completed_issues + module.cancelled_issues)}{" "} {moduleDetails.total_issues -
(moduleDetails.completed_issues + moduleDetails.cancelled_issues)}{" "}
</span> </span>
</div> </div>
@ -465,10 +487,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div> </div>
<div className="relative h-40 w-80"> <div className="relative h-40 w-80">
<ProgressChart <ProgressChart
distribution={module.distribution.completion_chart} distribution={moduleDetails.distribution.completion_chart}
startDate={module.start_date ?? ""} startDate={moduleDetails.start_date ?? ""}
endDate={module.target_date ?? ""} endDate={moduleDetails.target_date ?? ""}
totalIssues={module.total_issues} totalIssues={moduleDetails.total_issues}
/> />
</div> </div>
</div> </div>
@ -491,7 +513,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
<span className="font-medium text-custom-text-200">Other Information</span> <span className="font-medium text-custom-text-200">Other Information</span>
</div> </div>
{module.total_issues > 0 ? ( {moduleDetails.total_issues > 0 ? (
<Disclosure.Button className="p-1"> <Disclosure.Button className="p-1">
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" /> <ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</Disclosure.Button> </Disclosure.Button>
@ -506,20 +528,20 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div> </div>
<Transition show={open}> <Transition show={open}>
<Disclosure.Panel> <Disclosure.Panel>
{module.total_issues > 0 ? ( {moduleDetails.total_issues > 0 ? (
<> <>
<div className=" h-full w-full py-4"> <div className=" h-full w-full py-4">
<SidebarProgressStats <SidebarProgressStats
distribution={module.distribution} distribution={moduleDetails.distribution}
groupedIssues={{ groupedIssues={{
backlog: module.backlog_issues, backlog: moduleDetails.backlog_issues,
unstarted: module.unstarted_issues, unstarted: moduleDetails.unstarted_issues,
started: module.started_issues, started: moduleDetails.started_issues,
completed: module.completed_issues, completed: moduleDetails.completed_issues,
cancelled: module.cancelled_issues, cancelled: moduleDetails.cancelled_issues,
}} }}
totalIssues={module.total_issues} totalIssues={moduleDetails.total_issues}
module={module} module={moduleDetails}
/> />
</div> </div>
</> </>
@ -544,9 +566,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</button> </button>
</div> </div>
<div className="mt-2 space-y-2 hover:bg-custom-background-80"> <div className="mt-2 space-y-2 hover:bg-custom-background-80">
{memberRole && module.link_module && module.link_module.length > 0 ? ( {memberRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? (
<LinksList <LinksList
links={module.link_module} links={moduleDetails.link_module}
handleEditLink={handleEditLink} handleEditLink={handleEditLink}
handleDeleteLink={handleDeleteLink} handleDeleteLink={handleDeleteLink}
userAuth={memberRole} userAuth={memberRole}
@ -571,4 +593,4 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</div> </div>
</> </>
); );
}; });

View File

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

View File

@ -56,6 +56,19 @@ export const copyTextToClipboard = async (text: string) => {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
}; };
/**
* @description: This function copies the url to clipboard after prepending the origin URL to it
* @param {string} path
* @example:
* const text = copyUrlToClipboard("path");
* copied URL: origin_url/path
*/
export const copyUrlToClipboard = async (path: string) => {
const originUrl = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
await copyTextToClipboard(`${originUrl}/${path}`);
};
export const generateRandomColor = (string: string): string => { export const generateRandomColor = (string: string): string => {
if (!string) return "rgb(var(--color-primary-100))"; if (!string) return "rgb(var(--color-primary-100))";

View File

@ -1,65 +1,40 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
// icons // mobx store
import { ArrowLeft } from "lucide-react"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; import { AppLayout } from "layouts/app-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { CycleIssuesHeader } from "components/headers";
import { ExistingIssuesListModal } from "components/core"; import { ExistingIssuesListModal } from "components/core";
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles"; import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
import { CycleLayoutRoot } from "components/issues/issue-layouts"; import { CycleLayoutRoot } from "components/issues/issue-layouts";
// services
import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// ui // ui
import { BreadcrumbItem, Breadcrumbs, CustomMenu, ContrastIcon } from "@plane/ui";
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
// images // assets
import emptyCycle from "public/empty-state/cycle.svg"; import emptyCycle from "public/empty-state/cycle.svg";
// helpers // helpers
import { truncateText } from "helpers/string.helper";
import { getDateRangeStatus } from "helpers/date-time.helper"; import { getDateRangeStatus } from "helpers/date-time.helper";
// types
import { ISearchIssueResponse } from "types";
// fetch-keys
import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys";
import { CycleIssuesHeader } from "components/headers";
// services
const issueService = new IssueService();
const cycleService = new CycleService();
const SingleCycle: React.FC = () => { const SingleCycle: React.FC = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const [cycleSidebar, setCycleSidebar] = useState(true);
const [analyticsModal, setAnalyticsModal] = useState(false);
const [transferIssuesModal, setTransferIssuesModal] = useState(false); const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query;
const { user } = useUserAuth(); const { cycle: cycleStore } = useMobxStore();
const { setToastAlert } = useToast(); const { storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
const { data: cycles } = useSWR(
workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "all")
: null
);
const { data: cycleDetails, error } = useSWR( const { data: cycleDetails, error } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null, workspaceSlug && projectId && cycleId ? `CURRENT_CYCLE_DETAILS_${cycleId.toString()}` : null,
workspaceSlug && projectId && cycleId workspaceSlug && projectId && cycleId
? () => cycleService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) ? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
: null : null
); );
@ -68,119 +43,59 @@ const SingleCycle: React.FC = () => {
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
: "draft"; : "draft";
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { // TODO: add this function to bulk add issues to cycle
if (!workspaceSlug || !projectId) return; // const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
// if (!workspaceSlug || !projectId) return;
const payload = { // const payload = {
issues: data.map((i) => i.id), // issues: data.map((i) => i.id),
}; // };
await issueService // await issueService
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user) // .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
.catch(() => { // .catch(() => {
setToastAlert({ // setToastAlert({
type: "error", // type: "error",
title: "Error!", // title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.", // message: "Selected issues could not be added to the cycle. Please try again.",
}); // });
}); // });
}; // };
return ( return (
<IssueViewContextProvider> <AppLayout header={<CycleIssuesHeader />} withProjectWrapper>
{/* TODO: Update logic to bulk add issues to a cycle */}
<ExistingIssuesListModal <ExistingIssuesListModal
isOpen={cycleIssuesListModal} isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)} handleClose={() => setCycleIssuesListModal(false)}
searchParams={{ cycle: true }} searchParams={{ cycle: true }}
handleOnSubmit={handleAddIssuesToCycle} handleOnSubmit={async () => {}}
/> />
<ProjectAuthorizationWrapper {error ? (
breadcrumbs={ <EmptyState
<Breadcrumbs onBack={() => router.back()}> image={emptyCycle}
<Breadcrumbs.BreadcrumbItem title="Cycle does not exist"
link={ description="The cycle you are looking for does not exist or has been deleted."
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles`}> primaryButton={{
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}> text: "View other cycles",
<p className="truncate">{`${truncateText( onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/cycles`),
cycleDetails?.project_detail.name ?? "Project", }}
32 />
)} Cycles`}</p> ) : (
</a> <>
</Link> <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`}>
</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`}
>
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />} {cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<div className="h-full w-full">
<CycleLayoutRoot /> <CycleLayoutRoot />
</div>
</div> </div>
<CycleDetailsSidebar {cycleId && <CycleDetailsSidebar isOpen={!isSidebarCollapsed} cycleId={cycleId.toString()} />}
cycleStatus={cycleStatus} </div>
cycle={cycleDetails} </>
isOpen={cycleSidebar} )}
isCompleted={cycleStatus === "completed" ?? false} </AppLayout>
user={user}
/>
</>
)}
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
); );
}; };

View File

@ -1,134 +1,73 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
// services // mobx store
import { ModuleService } from "services/module.service"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; import { AppLayout } from "layouts/app-layout";
// components // components
import { ExistingIssuesListModal } from "components/core"; import { ExistingIssuesListModal } from "components/core";
import { ModuleDetailsSidebar } from "components/modules"; import { ModuleDetailsSidebar } from "components/modules";
import { ModuleLayoutRoot } from "components/issues"; import { ModuleLayoutRoot } from "components/issues";
import { ModuleIssuesHeader } from "components/headers"; import { ModuleIssuesHeader } from "components/headers";
// ui // ui
import { BreadcrumbItem, Breadcrumbs, CustomMenu, DiceIcon } from "@plane/ui";
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
// images // assets
import emptyModule from "public/empty-state/module.svg"; import emptyModule from "public/empty-state/module.svg";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { ISearchIssueResponse } from "types";
// fetch-keys
import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
// services
const moduleService = new ModuleService();
const SingleModule: React.FC = () => { const SingleModule: React.FC = () => {
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
const [moduleSidebar, setModuleSidebar] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query;
const { user } = useUserAuth(); const { module: moduleStore } = useMobxStore();
const { setToastAlert } = useToast(); const { storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
const { data: modules } = useSWR( const { error } = useSWR(
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, workspaceSlug && projectId && moduleId ? `CURRENT_MODULE_DETAILS_${moduleId.toString()}` : null,
workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null
);
const { data: moduleIssues } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_ISSUES(moduleId as string) : null,
workspaceSlug && projectId && moduleId workspaceSlug && projectId && moduleId
? () => moduleService.getModuleIssues(workspaceSlug as string, projectId as string, moduleId as string) ? () => moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
: null : null
); );
const { data: moduleDetails, error } = useSWR( // TODO: add this function to bulk add issues to cycle
moduleId ? MODULE_DETAILS(moduleId as string) : null, // const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
workspaceSlug && projectId // if (!workspaceSlug || !projectId) return;
? () => moduleService.getModuleDetails(workspaceSlug as string, projectId as string, moduleId as string)
: null
);
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { // const payload = {
if (!workspaceSlug || !projectId) return; // issues: data.map((i) => i.id),
// };
const payload = { // await moduleService
issues: data.map((i) => i.id), // .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user)
}; // .catch(() =>
// setToastAlert({
// type: "error",
// title: "Error!",
// message: "Selected issues could not be added to the module. Please try again.",
// })
// );
// };
await moduleService // const openIssuesListModal = () => {
.addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user) // setModuleIssuesListModal(true);
.catch(() => // };
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the module. Please try again.",
})
);
};
const openIssuesListModal = () => {
setModuleIssuesListModal(true);
};
return ( return (
<> <>
<ExistingIssuesListModal <AppLayout header={<ModuleIssuesHeader />} withProjectWrapper>
isOpen={moduleIssuesListModal} {/* TODO: Update logic to bulk add issues to a cycle */}
handleClose={() => setModuleIssuesListModal(false)} <ExistingIssuesListModal
searchParams={{ module: true }} isOpen={moduleIssuesListModal}
handleOnSubmit={handleAddIssuesToModule} handleClose={() => setModuleIssuesListModal(false)}
/> searchParams={{ module: true }}
<ProjectAuthorizationWrapper handleOnSubmit={async () => {}}
breadcrumbs={ />
<Breadcrumbs onBack={() => router.back()}>
<BreadcrumbItem
link={
<Link href={`/${workspaceSlug}/projects/${projectId}/modules`}>
<a className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm `}>
<p className="truncate">{`${truncateText(
moduleDetails?.project_detail.name ?? "Project",
32
)} Modules`}</p>
</a>
</Link>
}
/>
</Breadcrumbs>
}
left={
<CustomMenu
label={
<>
<DiceIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</>
}
className="ml-1.5"
width="auto"
>
{modules?.map((module) => (
<CustomMenu.MenuItem
key={module.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)}
>
{truncateText(module.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>
}
right={<ModuleIssuesHeader />}
>
{error ? ( {error ? (
<EmptyState <EmptyState
image={emptyModule} image={emptyModule}
@ -140,23 +79,14 @@ const SingleModule: React.FC = () => {
}} }}
/> />
) : ( ) : (
<> <div className="flex h-full w-full">
<div <div className={`h-full w-full ${isSidebarCollapsed ? "" : "mr-[24rem]"} duration-300`}>
className={`relative overflow-y-auto h-full flex flex-col ${
moduleSidebar ? "mr-[24rem]" : ""
} duration-300`}
>
<ModuleLayoutRoot /> <ModuleLayoutRoot />
</div> </div>
<ModuleDetailsSidebar {moduleId && <ModuleDetailsSidebar isOpen={!isSidebarCollapsed} moduleId={moduleId.toString()} />}
module={moduleDetails} </div>
isOpen={moduleSidebar}
moduleIssues={moduleIssues}
user={user}
/>
</>
)} )}
</ProjectAuthorizationWrapper> </AppLayout>
</> </>
); );
}; };

View File

@ -5,6 +5,7 @@ import { RootStore } from "../root";
import { IIssue } from "types"; import { IIssue } from "types";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
import { IssueService } from "services/issue";
// constants // constants
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
@ -34,6 +35,9 @@ export interface ICycleIssueStore {
// action // action
fetchIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>; fetchIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<any>;
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => void;
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => void;
} }
export class CycleIssueStore implements ICycleIssueStore { export class CycleIssueStore implements ICycleIssueStore {
@ -52,9 +56,11 @@ export class CycleIssueStore implements ICycleIssueStore {
ungrouped: IIssue[]; ungrouped: IIssue[];
}; };
} = {}; } = {};
// service
cycleService; // services
rootStore; rootStore;
cycleService;
issueService;
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
@ -68,10 +74,14 @@ export class CycleIssueStore implements ICycleIssueStore {
// actions // actions
fetchIssues: action, fetchIssues: action,
updateIssueStructure: action, updateIssueStructure: action,
deleteIssue: action,
addIssueToCycle: action,
removeIssueFromCycle: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
this.cycleService = new CycleService(); this.cycleService = new CycleService();
this.issueService = new IssueService();
autorun(() => { autorun(() => {
const workspaceSlug = this.rootStore.workspace.workspaceSlug; const workspaceSlug = this.rootStore.workspace.workspaceSlug;
@ -130,7 +140,7 @@ export class CycleIssueStore implements ICycleIssueStore {
issues = issues as IIssueGroupedStructure; issues = issues as IIssueGroupedStructure;
issues = { issues = {
...issues, ...issues,
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), [group_id]: issues[group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
}; };
} }
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
@ -139,27 +149,55 @@ export class CycleIssueStore implements ICycleIssueStore {
...issues, ...issues,
[sub_group_id]: { [sub_group_id]: {
...issues[sub_group_id], ...issues[sub_group_id],
[group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), [group_id]: issues[sub_group_id][group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
}, },
}; };
} }
if (issueType === "ungrouped") { if (issueType === "ungrouped") {
issues = issues as IIssueUnGroupedStructure; issues = issues as IIssueUnGroupedStructure;
issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); issues = issues.map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i));
} }
const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || "";
if (orderBy === "-created_at") { if (orderBy === "-created_at") issues = sortArrayByDate(issues as any, "created_at");
issues = sortArrayByDate(issues as any, "created_at"); if (orderBy === "-updated_at") issues = sortArrayByDate(issues as any, "updated_at");
if (orderBy === "start_date") issues = sortArrayByDate(issues as any, "updated_at");
if (orderBy === "priority") issues = sortArrayByPriority(issues as any, "priority");
runInAction(() => {
this.issues = { ...this.issues, [cycleId]: { ...this.issues[cycleId], [issueType]: issues } };
});
};
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const cycleId: string | null = this.rootStore.cycle.cycleId;
const issueType = this.getIssueType;
if (!cycleId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
this.getIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {
issues = issues as IIssueGroupedStructure;
issues = {
...issues,
[group_id]: issues[group_id].filter((i) => i?.id !== issue?.id),
};
} }
if (orderBy === "-updated_at") { if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
issues = sortArrayByDate(issues as any, "updated_at"); issues = issues as IIssueGroupWithSubGroupsStructure;
issues = {
...issues,
[sub_group_id]: {
...issues[sub_group_id],
[group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id),
},
};
} }
if (orderBy === "start_date") { if (issueType === "ungrouped") {
issues = sortArrayByDate(issues as any, "updated_at"); issues = issues as IIssueUnGroupedStructure;
} issues = issues.filter((i) => i?.id !== issue?.id);
if (orderBy === "priority") {
issues = sortArrayByPriority(issues as any, "priority");
} }
runInAction(() => { runInAction(() => {
@ -199,4 +237,44 @@ export class CycleIssueStore implements ICycleIssueStore {
return error; return error;
} }
}; };
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
try {
const user = this.rootStore.user.currentUser ?? undefined;
await this.issueService.addIssueToCycle(
workspaceSlug,
projectId,
cycleId,
{
issues: [issueId],
},
user
);
this.fetchIssues(workspaceSlug, projectId, cycleId);
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => {
try {
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, bridgeId);
} catch (error) {
this.fetchIssues(workspaceSlug, projectId, cycleId);
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
} }

View File

@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
// types // types
import { RootStore } from "../root"; import { RootStore } from "../root";
import { IIssueType } from "store/issue"; import { IIssueType } from "store/issue";
import { IUser } from "types";
export interface ICycleIssueKanBanViewStore { export interface ICycleIssueKanBanViewStore {
kanBanToggle: { kanBanToggle: {
@ -293,8 +292,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
updateIssue, updateIssue
{} as IUser
); );
} }
}; };
@ -442,8 +440,7 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore {
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
updateIssue, updateIssue
{} as IUser
); );
} }
}; };

View File

@ -33,6 +33,7 @@ export interface IIssueStore {
// action // action
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>; fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
} }
export class IssueStore implements IIssueStore { export class IssueStore implements IIssueStore {
@ -67,6 +68,7 @@ export class IssueStore implements IIssueStore {
// actions // actions
fetchIssues: action, fetchIssues: action,
updateIssueStructure: action, updateIssueStructure: action,
deleteIssue: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -163,6 +165,42 @@ export class IssueStore implements IIssueStore {
}); });
}; };
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const projectId: string | null = issue?.project;
const issueType = this.getIssueType;
if (!projectId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
this.getIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {
issues = issues as IIssueGroupedStructure;
issues = {
...issues,
[group_id]: issues[group_id].filter((i) => i?.id !== issue?.id),
};
}
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
issues = issues as IIssueGroupWithSubGroupsStructure;
issues = {
...issues,
[sub_group_id]: {
...issues[sub_group_id],
[group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id),
},
};
}
if (issueType === "ungrouped") {
issues = issues as IIssueUnGroupedStructure;
issues = issues.filter((i) => i?.id !== issue?.id);
}
runInAction(() => {
this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } };
});
};
fetchIssues = async (workspaceSlug: string, projectId: string) => { fetchIssues = async (workspaceSlug: string, projectId: string) => {
try { try {
this.loader = true; this.loader = true;

View File

@ -3,7 +3,7 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"
import { IssueService, IssueReactionService } from "services/issue"; import { IssueService, IssueReactionService } from "services/issue";
// types // types
import { RootStore } from "../root"; import { RootStore } from "../root";
import { IUser, IIssue } from "types"; import { IIssue } from "types";
// constants // constants
import { groupReactionEmojis } from "constants/issue"; import { groupReactionEmojis } from "constants/issue";
@ -36,17 +36,11 @@ export interface IIssueDetailStore {
// fetch issue details // fetch issue details
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>; fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
// creating issue // creating issue
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => Promise<IIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => Promise<IIssue>;
// updating issue // updating issue
updateIssue: ( updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial<IIssue>) => void;
workspaceId: string,
projectId: string,
issueId: string,
data: Partial<IIssue>,
user: IUser | undefined
) => void;
// deleting issue // deleting issue
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => void; deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>; fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
@ -210,13 +204,15 @@ export class IssueDetailStore implements IIssueDetailStore {
} }
}; };
createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => { createIssue = async (workspaceSlug: string, projectId: string, data: Partial<IIssue>) => {
try { try {
runInAction(() => { runInAction(() => {
this.loader = true; this.loader = true;
this.error = null; this.error = null;
}); });
const user = this.rootStore.user.currentUser ?? undefined;
const response = await this.issueService.createIssue(workspaceSlug, projectId, data, user); const response = await this.issueService.createIssue(workspaceSlug, projectId, data, user);
runInAction(() => { runInAction(() => {
@ -237,13 +233,7 @@ export class IssueDetailStore implements IIssueDetailStore {
} }
}; };
updateIssue = async ( updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => {
workspaceSlug: string,
projectId: string,
issueId: string,
data: Partial<IIssue>,
user: IUser | undefined
) => {
const newIssues = { ...this.issues }; const newIssues = { ...this.issues };
newIssues[issueId] = { newIssues[issueId] = {
...newIssues[issueId], ...newIssues[issueId],
@ -257,6 +247,10 @@ export class IssueDetailStore implements IIssueDetailStore {
this.issues = newIssues; this.issues = newIssues;
}); });
const user = this.rootStore.user.currentUser;
if (!user) return;
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data, user); const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data, user);
runInAction(() => { runInAction(() => {
@ -282,7 +276,7 @@ export class IssueDetailStore implements IIssueDetailStore {
} }
}; };
deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => { deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
const newIssues = { ...this.issues }; const newIssues = { ...this.issues };
delete newIssues[issueId]; delete newIssues[issueId];
@ -293,12 +287,18 @@ export class IssueDetailStore implements IIssueDetailStore {
this.issues = newIssues; this.issues = newIssues;
}); });
await this.issueService.deleteIssue(workspaceSlug, projectId, issueId, user); const user = this.rootStore.user.currentUser;
if (!user) return;
const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId, user);
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = null; this.error = null;
}); });
return response;
} catch (error) { } catch (error) {
this.fetchIssueDetails(workspaceSlug, projectId, issueId); this.fetchIssueDetails(workspaceSlug, projectId, issueId);

View File

@ -123,7 +123,7 @@ export class IssueFilterStore implements IIssueFilterStore {
labels: this.userFilters?.labels || undefined, labels: this.userFilters?.labels || undefined,
start_date: this.userFilters?.start_date || undefined, start_date: this.userFilters?.start_date || undefined,
target_date: this.userFilters?.target_date || undefined, target_date: this.userFilters?.target_date || undefined,
group_by: this.userDisplayFilters?.group_by || "state", group_by: this.userDisplayFilters?.group_by || undefined,
order_by: this.userDisplayFilters?.order_by || "-created_at", order_by: this.userDisplayFilters?.order_by || "-created_at",
sub_group_by: this.userDisplayFilters?.sub_group_by || undefined, sub_group_by: this.userDisplayFilters?.sub_group_by || undefined,
type: this.userDisplayFilters?.type || undefined, type: this.userDisplayFilters?.type || undefined,

View File

@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
// types // types
import { RootStore } from "../root"; import { RootStore } from "../root";
import { IIssueType } from "./issue.store"; import { IIssueType } from "./issue.store";
import { IUser } from "types";
export interface IIssueKanBanViewStore { export interface IIssueKanBanViewStore {
kanBanToggle: { kanBanToggle: {
@ -293,8 +292,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
updateIssue, updateIssue
{} as IUser
); );
} }
}; };
@ -442,8 +440,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
updateIssue, updateIssue
{} as IUser
); );
} }
}; };

View File

@ -34,6 +34,9 @@ export interface IModuleIssueStore {
// action // action
fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<any>; fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<any>;
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<any>;
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => Promise<any>;
} }
export class ModuleIssueStore implements IModuleIssueStore { export class ModuleIssueStore implements IModuleIssueStore {
@ -52,9 +55,10 @@ export class ModuleIssueStore implements IModuleIssueStore {
ungrouped: IIssue[]; ungrouped: IIssue[];
}; };
} = {}; } = {};
// service
moduleService; // services
rootStore; rootStore;
moduleService;
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
@ -68,6 +72,9 @@ export class ModuleIssueStore implements IModuleIssueStore {
// actions // actions
fetchIssues: action, fetchIssues: action,
updateIssueStructure: action, updateIssueStructure: action,
deleteIssue: action,
addIssueToModule: action,
removeIssueFromModule: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -167,6 +174,42 @@ export class ModuleIssueStore implements IModuleIssueStore {
}); });
}; };
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const moduleId: string | null = this.rootStore.module.moduleId;
const issueType = this.getIssueType;
if (!moduleId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
this.getIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {
issues = issues as IIssueGroupedStructure;
issues = {
...issues,
[group_id]: issues[group_id].filter((i) => i?.id !== issue?.id),
};
}
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
issues = issues as IIssueGroupWithSubGroupsStructure;
issues = {
...issues,
[sub_group_id]: {
...issues[sub_group_id],
[group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id),
},
};
}
if (issueType === "ungrouped") {
issues = issues as IIssueUnGroupedStructure;
issues = issues.filter((i) => i?.id !== issue?.id);
}
runInAction(() => {
this.issues = { ...this.issues, [moduleId]: { ...this.issues[moduleId], [issueType]: issues } };
});
};
fetchIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => { fetchIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try { try {
this.loader = true; this.loader = true;
@ -204,4 +247,44 @@ export class ModuleIssueStore implements IModuleIssueStore {
return error; return error;
} }
}; };
addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
try {
const user = this.rootStore.user.currentUser ?? undefined;
await this.moduleService.addIssuesToModule(
workspaceSlug,
projectId,
moduleId,
{
issues: [issueId],
},
user
);
this.fetchIssues(workspaceSlug, projectId, moduleId);
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => {
try {
await this.moduleService.removeIssueFromModule(workspaceSlug, projectId, moduleId, bridgeId);
} catch (error) {
this.fetchIssues(workspaceSlug, projectId, moduleId);
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
} }

View File

@ -2,7 +2,6 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
// types // types
import { RootStore } from "../root"; import { RootStore } from "../root";
import { IIssueType } from "../issue/issue.store"; import { IIssueType } from "../issue/issue.store";
import { IUser } from "types";
export interface IModuleIssueKanBanViewStore { export interface IModuleIssueKanBanViewStore {
kanBanToggle: { kanBanToggle: {
@ -293,8 +292,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
updateIssue, updateIssue
this.rootStore.user.currentUser as IUser
); );
} }
}; };
@ -442,8 +440,7 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore {
updateIssue.workspaceSlug, updateIssue.workspaceSlug,
updateIssue.projectId, updateIssue.projectId,
updateIssue.issueId, updateIssue.issueId,
updateIssue, updateIssue
this.rootStore.user.currentUser as IUser
); );
} }
}; };

View File

@ -38,7 +38,7 @@ export interface IModuleStore {
getModuleById: (moduleId: string) => IModule | null; getModuleById: (moduleId: string) => IModule | null;
fetchModules: (workspaceSlug: string, projectId: string) => void; fetchModules: (workspaceSlug: string, projectId: string) => void;
fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => void; fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<IModule>;
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>; createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => void; updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => void;
@ -171,8 +171,6 @@ export class ModuleStore implements IModuleStore {
const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId);
if (!response) return null;
runInAction(() => { runInAction(() => {
this.moduleDetails = { this.moduleDetails = {
...this.moduleDetails, ...this.moduleDetails,
@ -181,6 +179,8 @@ export class ModuleStore implements IModuleStore {
this.loader = false; this.loader = false;
this.error = null; this.error = null;
}); });
return response;
} catch (error) { } catch (error) {
console.error("Failed to fetch module details in module store", error); console.error("Failed to fetch module details in module store", error);
@ -188,6 +188,8 @@ export class ModuleStore implements IModuleStore {
this.loader = false; this.loader = false;
this.error = error; this.error = error;
}); });
throw error;
} }
}; };

View File

@ -36,6 +36,7 @@ export interface IProfileIssueStore {
// action // action
fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise<any>; fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise<any>;
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
} }
export class ProfileIssueStore implements IProfileIssueStore { export class ProfileIssueStore implements IProfileIssueStore {
@ -76,6 +77,7 @@ export class ProfileIssueStore implements IProfileIssueStore {
// actions // actions
fetchIssues: action, fetchIssues: action,
updateIssueStructure: action, updateIssueStructure: action,
deleteIssue: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
this.userService = new UserService(); this.userService = new UserService();
@ -174,6 +176,55 @@ export class ProfileIssueStore implements IProfileIssueStore {
}); });
}; };
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const workspaceSlug: string | null = this.rootStore?.workspace?.workspaceSlug;
const userId: string | null = this.userId;
const issueType = this.getIssueType;
if (!workspaceSlug || !userId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
this.getIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {
issues = issues as IIssueGroupedStructure;
issues = {
...issues,
[group_id]: issues[group_id].filter((i: IIssue) => i?.id !== issue?.id),
};
}
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
issues = issues as IIssueGroupWithSubGroupsStructure;
issues = {
...issues,
[sub_group_id]: {
...issues[sub_group_id],
[group_id]: issues[sub_group_id][group_id].filter((i: IIssue) => i?.id !== issue?.id),
},
};
}
if (issueType === "ungrouped") {
issues = issues as IIssueUnGroupedStructure;
issues = issues.filter((i: IIssue) => i?.id !== issue?.id);
}
runInAction(() => {
this.issues = {
...this.issues,
[workspaceSlug]: {
...this.issues?.[workspaceSlug],
[userId]: {
...this.issues?.[workspaceSlug]?.[userId],
[issueType]: issues,
},
},
};
});
};
fetchIssues = async ( fetchIssues = async (
workspaceSlug: string, workspaceSlug: string,
userId: string, userId: string,

View File

@ -30,6 +30,7 @@ export interface IProjectViewIssuesStore {
// actions // actions
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
fetchViewIssues: ( fetchViewIssues: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -72,6 +73,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
// actions // actions
updateIssueStructure: action, updateIssueStructure: action,
deleteIssue: action,
fetchViewIssues: action, fetchViewIssues: action,
// computed // computed
@ -167,6 +169,42 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore {
}); });
}; };
deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const viewId: string | null = this.rootStore.projectViews.viewId;
const issueType = this.rootStore.issue.getIssueType;
if (!viewId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
this.getIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {
issues = issues as IIssueGroupedStructure;
issues = {
...issues,
[group_id]: issues[group_id].filter((i) => i?.id !== issue?.id),
};
}
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
issues = issues as IIssueGroupWithSubGroupsStructure;
issues = {
...issues,
[sub_group_id]: {
...issues[sub_group_id],
[group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id),
},
};
}
if (issueType === "ungrouped") {
issues = issues as IIssueUnGroupedStructure;
issues = issues.filter((i) => i?.id !== issue?.id);
}
runInAction(() => {
this.viewIssues = { ...this.viewIssues, [viewId]: { ...this.viewIssues[viewId], [issueType]: issues } };
});
};
fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => { fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => {
try { try {
runInAction(() => { runInAction(() => {