mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
style: cycle ui revamp, and chore: code refactor (#2558)
* chore: cycle custom svg icon added and code refactor * chore: module code refactor * style: cycle ui revamp and code refactor * chore: cycle card view layout fix * chore: layout fix * style: module and cycle title tooltip position
This commit is contained in:
parent
7edaa49c21
commit
8eaac60aa5
20
packages/ui/src/icons/cycle/circle-dot-full-icon.tsx
Normal file
20
packages/ui/src/icons/cycle/circle-dot-full-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ISvgIcons } from "../type";
|
||||||
|
|
||||||
|
export const CircleDotFullIcon: React.FC<ISvgIcons> = ({
|
||||||
|
className = "text-current",
|
||||||
|
...rest
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className={`${className} stroke-2`}
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<circle cx="8.33333" cy="8.33333" r="5.33333" stroke-linecap="round" />
|
||||||
|
<circle cx="8.33333" cy="8.33333" r="4.33333" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { ISvgIcons } from "./type";
|
import { ISvgIcons } from "../type";
|
||||||
|
|
||||||
export const ContrastIcon: React.FC<ISvgIcons> = ({
|
export const ContrastIcon: React.FC<ISvgIcons> = ({
|
||||||
className = "text-current",
|
className = "text-current",
|
33
packages/ui/src/icons/cycle/cycle-group-icon.tsx
Normal file
33
packages/ui/src/icons/cycle/cycle-group-icon.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ContrastIcon } from "./contrast-icon";
|
||||||
|
import { CircleDotFullIcon } from "./circle-dot-full-icon";
|
||||||
|
import { CircleDotDashed, Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { CYCLE_GROUP_COLORS, ICycleGroupIcon } from "./helper";
|
||||||
|
|
||||||
|
const iconComponents = {
|
||||||
|
current: ContrastIcon,
|
||||||
|
upcoming: CircleDotDashed,
|
||||||
|
completed: CircleDotFullIcon,
|
||||||
|
draft: Circle,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CycleGroupIcon: React.FC<ICycleGroupIcon> = ({
|
||||||
|
className = "",
|
||||||
|
color,
|
||||||
|
cycleGroup,
|
||||||
|
height = "12px",
|
||||||
|
width = "12px",
|
||||||
|
}) => {
|
||||||
|
const CycleIconComponent = iconComponents[cycleGroup] || ContrastIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CycleIconComponent
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
color={color ?? CYCLE_GROUP_COLORS[cycleGroup]}
|
||||||
|
className={`flex-shrink-0 ${className}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { ISvgIcons } from "./type";
|
import { ISvgIcons } from "../type";
|
||||||
|
|
||||||
export const DoubleCircleIcon: React.FC<ISvgIcons> = ({
|
export const DoubleCircleIcon: React.FC<ISvgIcons> = ({
|
||||||
className = "text-current",
|
className = "text-current",
|
18
packages/ui/src/icons/cycle/helper.tsx
Normal file
18
packages/ui/src/icons/cycle/helper.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface ICycleGroupIcon {
|
||||||
|
className?: string;
|
||||||
|
color?: string;
|
||||||
|
cycleGroup: TCycleGroups;
|
||||||
|
height?: string;
|
||||||
|
width?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCycleGroups = "current" | "upcoming" | "completed" | "draft";
|
||||||
|
|
||||||
|
export const CYCLE_GROUP_COLORS: {
|
||||||
|
[key in TCycleGroups]: string;
|
||||||
|
} = {
|
||||||
|
current: "#F59E0B",
|
||||||
|
upcoming: "#3F76FF",
|
||||||
|
completed: "#16A34A",
|
||||||
|
draft: "#525252",
|
||||||
|
};
|
5
packages/ui/src/icons/cycle/index.ts
Normal file
5
packages/ui/src/icons/cycle/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./double-circle-icon";
|
||||||
|
export * from "./circle-dot-full-icon";
|
||||||
|
export * from "./contrast-icon";
|
||||||
|
export * from "./circle-dot-full-icon";
|
||||||
|
export * from "./cycle-group-icon";
|
@ -1,5 +1,4 @@
|
|||||||
export * from "./user-group-icon";
|
export * from "./user-group-icon";
|
||||||
export * from "./contrast-icon";
|
|
||||||
export * from "./dice-icon";
|
export * from "./dice-icon";
|
||||||
export * from "./layers-icon";
|
export * from "./layers-icon";
|
||||||
export * from "./photo-filter-icon";
|
export * from "./photo-filter-icon";
|
||||||
@ -7,7 +6,6 @@ export * from "./archive-icon";
|
|||||||
export * from "./admin-profile-icon";
|
export * from "./admin-profile-icon";
|
||||||
export * from "./create-icon";
|
export * from "./create-icon";
|
||||||
export * from "./subscribe-icon";
|
export * from "./subscribe-icon";
|
||||||
export * from "./double-circle-icon";
|
|
||||||
export * from "./external-link-icon";
|
export * from "./external-link-icon";
|
||||||
export * from "./copy-icon";
|
export * from "./copy-icon";
|
||||||
export * from "./layer-stack";
|
export * from "./layer-stack";
|
||||||
@ -20,6 +18,7 @@ export * from "./blocked-icon";
|
|||||||
export * from "./blocker-icon";
|
export * from "./blocker-icon";
|
||||||
export * from "./related-icon";
|
export * from "./related-icon";
|
||||||
export * from "./module";
|
export * from "./module";
|
||||||
|
export * from "./cycle";
|
||||||
export * from "./github-icon";
|
export * from "./github-icon";
|
||||||
export * from "./discord-icon";
|
export * from "./discord-icon";
|
||||||
export * from "./transfer-icon";
|
export * from "./transfer-icon";
|
||||||
|
@ -36,7 +36,7 @@ type Props = {
|
|||||||
module?: IModule;
|
module?: IModule;
|
||||||
roundedTab?: boolean;
|
roundedTab?: boolean;
|
||||||
noBackground?: boolean;
|
noBackground?: boolean;
|
||||||
isPeekModuleDetails?: boolean;
|
isPeekView?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarProgressStats: React.FC<Props> = ({
|
export const SidebarProgressStats: React.FC<Props> = ({
|
||||||
@ -46,7 +46,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
module,
|
module,
|
||||||
roundedTab,
|
roundedTab,
|
||||||
noBackground,
|
noBackground,
|
||||||
isPeekModuleDetails = false,
|
isPeekView = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { filters, setFilters } = useIssuesView();
|
const { filters, setFilters } = useIssuesView();
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
completed={assignee.completed_issues}
|
completed={assignee.completed_issues}
|
||||||
total={assignee.total_issues}
|
total={assignee.total_issues}
|
||||||
{...(!isPeekModuleDetails && {
|
{...(!isPeekView && {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
|
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
|
||||||
setFilters({
|
setFilters({
|
||||||
@ -213,7 +213,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
completed={label.completed_issues}
|
completed={label.completed_issues}
|
||||||
total={label.total_issues}
|
total={label.total_issues}
|
||||||
{...(!isPeekModuleDetails && {
|
{...(!isPeekView && {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (filters.labels?.includes(label.label_id ?? ""))
|
if (filters.labels?.includes(label.label_id ?? ""))
|
||||||
setFilters({
|
setFilters({
|
||||||
|
55
web/components/cycles/cycle-peek-overview.tsx
Normal file
55
web/components/cycles/cycle-peek-overview.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { CycleDetailsSidebar } from "./sidebar";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { peekCycle } = router.query;
|
||||||
|
|
||||||
|
const ref = React.useRef(null);
|
||||||
|
|
||||||
|
const { cycle: cycleStore } = useMobxStore();
|
||||||
|
|
||||||
|
const { fetchCycleWithId } = cycleStore;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
delete router.query.peekCycle;
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...router.query },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!peekCycle) return;
|
||||||
|
fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString());
|
||||||
|
}, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{peekCycle && (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="flex flex-col gap-3.5 h-full w-[24rem] z-10 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 flex-shrink-0"
|
||||||
|
style={{
|
||||||
|
boxShadow:
|
||||||
|
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CycleDetailsSidebar cycleId={peekCycle?.toString() ?? ""} handleClose={handleClose} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -1,64 +1,32 @@
|
|||||||
import { FC, MouseEvent, useState } from "react";
|
import { FC, MouseEvent, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// next imports
|
// next imports
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// headless ui
|
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { SingleProgressStats } from "components/core";
|
|
||||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { AssigneesList } from "components/ui/avatar";
|
import { AssigneesList } from "components/ui/avatar";
|
||||||
import { CustomMenu, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
|
import { CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
|
||||||
AlarmClock,
|
|
||||||
AlertTriangle,
|
|
||||||
ArrowRight,
|
|
||||||
CalendarDays,
|
|
||||||
ChevronDown,
|
|
||||||
LinkIcon,
|
|
||||||
Pencil,
|
|
||||||
Star,
|
|
||||||
Target,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
import {
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
getDateRangeStatus,
|
||||||
|
findHowManyDaysLeft,
|
||||||
|
renderShortDate,
|
||||||
|
renderShortMonthDate,
|
||||||
|
} from "helpers/date-time.helper";
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// constants
|
||||||
const stateGroups = [
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
{
|
|
||||||
key: "backlog_issues",
|
|
||||||
title: "Backlog",
|
|
||||||
color: "#dee2e6",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "unstarted_issues",
|
|
||||||
title: "Unstarted",
|
|
||||||
color: "#26b5ce",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "started_issues",
|
|
||||||
title: "Started",
|
|
||||||
color: "#f7ae59",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "cancelled_issues",
|
|
||||||
title: "Cancelled",
|
|
||||||
color: "#d687ff",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "completed_issues",
|
|
||||||
title: "Completed",
|
|
||||||
color: "#09a953",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface ICyclesBoardCard {
|
export interface ICyclesBoardCard {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -81,7 +49,34 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
const endDate = new Date(cycle.end_date ?? "");
|
const endDate = new Date(cycle.end_date ?? "");
|
||||||
const startDate = new Date(cycle.start_date ?? "");
|
const startDate = new Date(cycle.start_date ?? "");
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const router = useRouter();
|
||||||
|
|
||||||
|
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||||
|
|
||||||
|
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
||||||
|
|
||||||
|
const cycleTotalIssues =
|
||||||
|
cycle.backlog_issues +
|
||||||
|
cycle.unstarted_issues +
|
||||||
|
cycle.started_issues +
|
||||||
|
cycle.completed_issues +
|
||||||
|
cycle.cancelled_issues;
|
||||||
|
|
||||||
|
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
|
||||||
|
|
||||||
|
const issueCount = cycle
|
||||||
|
? cycleTotalIssues === 0
|
||||||
|
? "0 Issue"
|
||||||
|
: cycleTotalIssues === cycle.completed_issues
|
||||||
|
? cycleTotalIssues > 1
|
||||||
|
? `${cycleTotalIssues} Issues`
|
||||||
|
: `${cycleTotalIssues} Issue`
|
||||||
|
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
|
||||||
|
: "0 Issue";
|
||||||
|
|
||||||
|
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
|
||||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
|
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
|
||||||
@ -93,21 +88,6 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const progressIndicatorData = stateGroups.map((group, index) => ({
|
|
||||||
id: index,
|
|
||||||
name: group.title,
|
|
||||||
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
|
|
||||||
color: group.color,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const groupedIssues: any = {
|
|
||||||
backlog: cycle.backlog_issues,
|
|
||||||
unstarted: cycle.unstarted_issues,
|
|
||||||
started: cycle.started_issues,
|
|
||||||
completed: cycle.completed_issues,
|
|
||||||
cancelled: cycle.cancelled_issues,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -134,6 +114,29 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setUpdateModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
const { query } = router;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekCycle: cycle.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CycleCreateUpdateModal
|
<CycleCreateUpdateModal
|
||||||
@ -152,267 +155,119 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col rounded-[10px] bg-custom-background-100 border border-custom-border-200 text-xs shadow">
|
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
<a className="flex flex-col justify-between p-4 h-44 w-full min-w-[250px] text-sm rounded bg-custom-background-100 border border-custom-border-100 hover:shadow-md">
|
||||||
<a className="w-full">
|
<div>
|
||||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center justify-between gap-1">
|
<div className="flex items-center gap-3">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex-shrink-0">
|
||||||
<span className="h-5 w-5">
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
|
||||||
<ContrastIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
color={`${
|
|
||||||
cycleStatus === "current"
|
|
||||||
? "#09A953"
|
|
||||||
: cycleStatus === "upcoming"
|
|
||||||
? "#F7AE59"
|
|
||||||
: cycleStatus === "completed"
|
|
||||||
? "#3F76FF"
|
|
||||||
: cycleStatus === "draft"
|
|
||||||
? "rgb(var(--color-text-200))"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
|
|
||||||
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 15)}</h3>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 capitalize">
|
<Tooltip tooltipContent={cycle.name} position="top">
|
||||||
|
<span className="text-base font-medium truncate">{cycle.name}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{currentCycle && (
|
||||||
<span
|
<span
|
||||||
className={`rounded-full px-1.5 py-0.5
|
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
|
||||||
${
|
style={{
|
||||||
cycleStatus === "current"
|
color: currentCycle.color,
|
||||||
? "bg-green-600/5 text-green-600"
|
backgroundColor: `${currentCycle.color}20`,
|
||||||
: cycleStatus === "upcoming"
|
}}
|
||||||
? "bg-orange-300/5 text-orange-300"
|
|
||||||
: cycleStatus === "completed"
|
|
||||||
? "bg-blue-500/5 text-blue-500"
|
|
||||||
: cycleStatus === "draft"
|
|
||||||
? "bg-neutral-400/5 text-neutral-400"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{cycleStatus === "current" ? (
|
{currentCycle.value === "current"
|
||||||
<span className="flex gap-1 whitespace-nowrap">
|
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
|
||||||
<RunningIcon className="h-4 w-4" />
|
: `${currentCycle.label}`}
|
||||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
|
||||||
</span>
|
|
||||||
) : cycleStatus === "upcoming" ? (
|
|
||||||
<span className="flex gap-1 whitespace-nowrap">
|
|
||||||
<AlarmClock className="h-4 w-4" />
|
|
||||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
|
||||||
</span>
|
|
||||||
) : cycleStatus === "completed" ? (
|
|
||||||
<span className="flex gap-1 whitespace-nowrap">
|
|
||||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
|
|
||||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<AlertTriangle className="h-3.5 w-3.5" />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}{" "}
|
|
||||||
Completed
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
cycleStatus
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
{cycle.is_favorite ? (
|
|
||||||
<button onClick={handleRemoveFromFavorites}>
|
|
||||||
<Star className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button onClick={handleAddToFavorites}>
|
|
||||||
<Star className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-4 items-center justify-start gap-5 text-custom-text-200">
|
|
||||||
{cycleStatus !== "draft" && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-start gap-1">
|
|
||||||
<CalendarDays className="h-4 w-4" />
|
|
||||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
|
||||||
</div>
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
<div className="flex items-start gap-1">
|
|
||||||
<Target className="h-4 w-4" />
|
|
||||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<button onClick={openCycleOverview}>
|
||||||
|
<Info className="h-4 w-4 text-custom-text-400" />
|
||||||
<div className="flex justify-between items-end">
|
</button>
|
||||||
<div className="flex flex-col gap-2 text-xs text-custom-text-200">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-16">Creator:</div>
|
|
||||||
<div className="flex items-center gap-2.5 text-custom-text-200">
|
|
||||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
|
||||||
<img
|
|
||||||
src={cycle.owned_by.avatar}
|
|
||||||
height={16}
|
|
||||||
width={16}
|
|
||||||
className="rounded-full"
|
|
||||||
alt={cycle.owned_by.display_name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
|
||||||
{cycle.owned_by.display_name.charAt(0)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-5 items-center gap-2">
|
|
||||||
<div className="w-16">Members:</div>
|
|
||||||
{cycle.assignees.length > 0 ? (
|
|
||||||
<div className="flex items-center gap-1 text-custom-text-200">
|
|
||||||
<AssigneesList users={cycle.assignees} length={4} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
"No members"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
{!isCompleted && (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setUpdateModal(true);
|
|
||||||
}}
|
|
||||||
className="cursor-pointer rounded p-1 text-custom-text-200 duration-300 hover:bg-custom-background-80"
|
|
||||||
>
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomMenu width="auto" verticalEllipsis>
|
|
||||||
{!isCompleted && (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDeleteModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
<span>Delete cycle</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleCopyText();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<LinkIcon className="h-4 w-4" />
|
|
||||||
<span>Copy cycle link</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="flex h-full flex-col rounded-b-[10px]">
|
<div className="flex flex-col gap-3">
|
||||||
<Disclosure>
|
<div className="flex items-center justify-between">
|
||||||
{({ open }) => (
|
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||||
<div
|
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||||
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-custom-border-200 bg-custom-background-80 text-custom-text-200 ${
|
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
||||||
open ? "" : "flex-row"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center gap-2 px-4 py-1">
|
|
||||||
<span>Progress</span>
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent={
|
|
||||||
<div className="flex w-56 flex-col">
|
|
||||||
{Object.keys(groupedIssues).map((group, index) => (
|
|
||||||
<SingleProgressStats
|
|
||||||
key={index}
|
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className="block h-3 w-3 rounded-full "
|
|
||||||
style={{
|
|
||||||
backgroundColor: stateGroups[index].color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="text-xs capitalize">{group}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
completed={groupedIssues[group]}
|
|
||||||
total={cycle.total_issues}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center">
|
|
||||||
<LinearProgressIndicator data={progressIndicatorData} noTooltip={true} />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Disclosure.Button>
|
|
||||||
<span className="p-1">
|
|
||||||
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</Disclosure.Button>
|
|
||||||
</div>
|
|
||||||
<Transition show={open}>
|
|
||||||
<Disclosure.Panel>
|
|
||||||
<div className="overflow-hidden rounded-b-md bg-custom-background-80 py-3 shadow">
|
|
||||||
<div className="col-span-2 space-y-3 px-4">
|
|
||||||
<div className="space-y-3 text-xs">
|
|
||||||
{stateGroups.map((group) => (
|
|
||||||
<div key={group.key} className="flex items-center justify-between gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className="block h-2 w-2 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: group.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<h6 className="text-xs">{group.title}</h6>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{cycle[group.key as keyof ICycle] as number}{" "}
|
|
||||||
<span className="text-custom-text-200">
|
|
||||||
-{" "}
|
|
||||||
{cycle.total_issues > 0
|
|
||||||
? `${Math.round(
|
|
||||||
((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100
|
|
||||||
)}%`
|
|
||||||
: "0%"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{cycle.assignees.length > 0 && (
|
||||||
</Disclosure>
|
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
|
||||||
</div>
|
<div className="flex items-center gap-1 cursor-default">
|
||||||
</div>
|
<AssigneesList users={cycle.assignees} length={3} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
|
||||||
|
position="top-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center w-full">
|
||||||
|
<div
|
||||||
|
className="bar relative h-1.5 w-full rounded bg-custom-background-90"
|
||||||
|
style={{
|
||||||
|
boxShadow: "1px 1px 4px 0px rgba(161, 169, 191, 0.35) inset",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 h-1.5 rounded bg-blue-600 duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${isNaN(completionPercentage) ? 0 : completionPercentage.toFixed(0)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-custom-text-300">
|
||||||
|
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "}
|
||||||
|
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1.5 z-10">
|
||||||
|
{cycle.is_favorite ? (
|
||||||
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
|
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" onClick={handleAddToFavorites}>
|
||||||
|
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<CustomMenu width="auto" ellipsis className="z-10">
|
||||||
|
{!isCompleted && (
|
||||||
|
<>
|
||||||
|
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
<span>Edit cycle</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
<span>Delete module</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<LinkIcon className="h-3 w-3" />
|
||||||
|
<span>Copy cycle link</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,26 +2,41 @@ import { FC } from "react";
|
|||||||
// types
|
// types
|
||||||
import { ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
// components
|
// components
|
||||||
import { CyclesBoardCard } from "components/cycles";
|
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
||||||
|
|
||||||
export interface ICyclesBoard {
|
export interface ICyclesBoard {
|
||||||
cycles: ICycle[];
|
cycles: ICycle[];
|
||||||
filter: string;
|
filter: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
peekCycle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CyclesBoard: FC<ICyclesBoard> = (props) => {
|
export const CyclesBoard: FC<ICyclesBoard> = (props) => {
|
||||||
const { cycles, filter, workspaceSlug, projectId } = props;
|
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
|
<>
|
||||||
{cycles.length > 0 ? (
|
{cycles.length > 0 ? (
|
||||||
<>
|
<div className="h-full w-full">
|
||||||
{cycles.map((cycle) => (
|
<div className="flex justify-between h-full w-full">
|
||||||
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} />
|
<div
|
||||||
))}
|
className={`grid grid-cols-1 gap-6 p-8 h-full w-full overflow-y-auto ${
|
||||||
</>
|
peekCycle
|
||||||
|
? "lg:grid-cols-1 xl:grid-cols-2 3xl:grid-cols-3"
|
||||||
|
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
|
||||||
|
} auto-rows-max transition-all `}
|
||||||
|
>
|
||||||
|
{cycles.map((cycle) => (
|
||||||
|
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<CyclePeekOverview
|
||||||
|
projectId={projectId?.toString() ?? ""}
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full grid place-items-center text-center">
|
<div className="h-full grid place-items-center text-center">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -50,6 +65,6 @@ export const CyclesBoard: FC<ICyclesBoard> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,29 +1,30 @@
|
|||||||
import { FC, MouseEvent, useState } from "react";
|
import { FC, MouseEvent, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// stores
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||||
|
import { AssigneesList } from "components/ui";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, RadialProgressBar, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
|
import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
|
||||||
AlarmClock,
|
|
||||||
AlertTriangle,
|
|
||||||
ArrowRight,
|
|
||||||
CalendarDays,
|
|
||||||
LinkIcon,
|
|
||||||
Pencil,
|
|
||||||
Star,
|
|
||||||
Target,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
import {
|
||||||
|
getDateRangeStatus,
|
||||||
|
findHowManyDaysLeft,
|
||||||
|
renderShortDate,
|
||||||
|
renderShortMonthDate,
|
||||||
|
} from "helpers/date-time.helper";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
// constants
|
||||||
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
|
|
||||||
type TCyclesListItem = {
|
type TCyclesListItem = {
|
||||||
cycle: ICycle;
|
cycle: ICycle;
|
||||||
@ -35,34 +36,6 @@ type TCyclesListItem = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateGroups = [
|
|
||||||
{
|
|
||||||
key: "backlog_issues",
|
|
||||||
title: "Backlog",
|
|
||||||
color: "#dee2e6",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "unstarted_issues",
|
|
||||||
title: "Unstarted",
|
|
||||||
color: "#26b5ce",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "started_issues",
|
|
||||||
title: "Started",
|
|
||||||
color: "#f7ae59",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "cancelled_issues",
|
|
||||||
title: "Cancelled",
|
|
||||||
color: "#d687ff",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "completed_issues",
|
|
||||||
title: "Completed",
|
|
||||||
color: "#09a953",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||||
const { cycle, workspaceSlug, projectId } = props;
|
const { cycle, workspaceSlug, projectId } = props;
|
||||||
// store
|
// store
|
||||||
@ -78,7 +51,28 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
const endDate = new Date(cycle.end_date ?? "");
|
const endDate = new Date(cycle.end_date ?? "");
|
||||||
const startDate = new Date(cycle.start_date ?? "");
|
const startDate = new Date(cycle.start_date ?? "");
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const router = useRouter();
|
||||||
|
|
||||||
|
const cycleTotalIssues =
|
||||||
|
cycle.backlog_issues +
|
||||||
|
cycle.unstarted_issues +
|
||||||
|
cycle.started_issues +
|
||||||
|
cycle.completed_issues +
|
||||||
|
cycle.cancelled_issues;
|
||||||
|
|
||||||
|
const renderDate = cycle.start_date || cycle.end_date;
|
||||||
|
|
||||||
|
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
||||||
|
|
||||||
|
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
|
||||||
|
|
||||||
|
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
|
||||||
|
|
||||||
|
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||||
|
|
||||||
|
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
|
||||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
|
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
|
||||||
@ -90,13 +84,6 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const progressIndicatorData = stateGroups.map((group, index) => ({
|
|
||||||
id: index,
|
|
||||||
name: group.title,
|
|
||||||
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
|
|
||||||
color: group.color,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -123,224 +110,31 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setUpdateModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
const { query } = router;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekCycle: cycle.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex items-center gap-1 hover:bg-custom-background-80 transition-all rounded px-2 pl-3">
|
|
||||||
<div className="w-full text-xs py-3">
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
|
||||||
<a className="w-full h-full relative overflow-hidden flex items-center gap-2">
|
|
||||||
{/* left content */}
|
|
||||||
<div className="relative flex items-center gap-2 overflow-hidden">
|
|
||||||
{/* cycle state */}
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<ContrastIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
color={`${
|
|
||||||
cycleStatus === "current"
|
|
||||||
? "#09A953"
|
|
||||||
: cycleStatus === "upcoming"
|
|
||||||
? "#F7AE59"
|
|
||||||
: cycleStatus === "completed"
|
|
||||||
? "#3F76FF"
|
|
||||||
: cycleStatus === "draft"
|
|
||||||
? "rgb(var(--color-text-200))"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* cycle title and description */}
|
|
||||||
<div className="max-w-xl">
|
|
||||||
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
|
|
||||||
<div className="text-base font-semibold line-clamp-1 pr-5 overflow-hidden break-words">
|
|
||||||
{cycle.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{cycle.description && (
|
|
||||||
<div className="mt-1 text-custom-text-200 break-words w-full line-clamp-2">{cycle.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* right content */}
|
|
||||||
<div className="ml-auto flex-shrink-0 relative flex items-center gap-3 p-2">
|
|
||||||
{/* cycle status */}
|
|
||||||
<div
|
|
||||||
className={`rounded-full px-2 py-1
|
|
||||||
${
|
|
||||||
cycleStatus === "current"
|
|
||||||
? "bg-green-600/10 text-green-600"
|
|
||||||
: cycleStatus === "upcoming"
|
|
||||||
? "bg-orange-300/10 text-orange-300"
|
|
||||||
: cycleStatus === "completed"
|
|
||||||
? "bg-blue-500/10 text-blue-500"
|
|
||||||
: cycleStatus === "draft"
|
|
||||||
? "bg-neutral-400/10 text-neutral-400"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{cycleStatus === "current" ? (
|
|
||||||
<span className="flex items-center gap-1 whitespace-nowrap">
|
|
||||||
<RunningIcon className="h-3.5 w-3.5" />
|
|
||||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} days left
|
|
||||||
</span>
|
|
||||||
) : cycleStatus === "upcoming" ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<AlarmClock className="h-3.5 w-3.5" />
|
|
||||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} days left
|
|
||||||
</span>
|
|
||||||
) : cycleStatus === "completed" ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
|
|
||||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<AlertTriangle className="h-3.5 w-3.5" />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}{" "}
|
|
||||||
Completed
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
cycleStatus
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* cycle start_date and target_date */}
|
|
||||||
{cycleStatus !== "draft" && (
|
|
||||||
<div className="flex items-center justify-start gap-2 text-custom-text-200">
|
|
||||||
<div className="flex items-start gap-1 whitespace-nowrap">
|
|
||||||
<CalendarDays className="h-4 w-4" />
|
|
||||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
|
|
||||||
<div className="flex items-start gap-1 whitespace-nowrap">
|
|
||||||
<Target className="h-4 w-4" />
|
|
||||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* cycle created by */}
|
|
||||||
<div className="flex items-center text-custom-text-200">
|
|
||||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
|
||||||
<img
|
|
||||||
src={cycle.owned_by.avatar}
|
|
||||||
height={16}
|
|
||||||
width={16}
|
|
||||||
className="rounded-full"
|
|
||||||
alt={cycle.owned_by.display_name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
|
||||||
{cycle.owned_by.display_name.charAt(0)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* cycle progress */}
|
|
||||||
<Tooltip
|
|
||||||
position="top-right"
|
|
||||||
tooltipContent={
|
|
||||||
<div className="flex w-80 items-center gap-2 px-4 py-1">
|
|
||||||
<span>Progress</span>
|
|
||||||
<LinearProgressIndicator data={progressIndicatorData} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`rounded-md px-1.5 py-1
|
|
||||||
${
|
|
||||||
cycleStatus === "current"
|
|
||||||
? "border border-green-600 bg-green-600/5 text-green-600"
|
|
||||||
: cycleStatus === "upcoming"
|
|
||||||
? "border border-orange-300 bg-orange-300/5 text-orange-300"
|
|
||||||
: cycleStatus === "completed"
|
|
||||||
? "border border-blue-500 bg-blue-500/5 text-blue-500"
|
|
||||||
: cycleStatus === "draft"
|
|
||||||
? "border border-neutral-400 bg-neutral-400/5 text-neutral-400"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{cycleStatus === "current" ? (
|
|
||||||
<span className="flex gap-1 whitespace-nowrap">
|
|
||||||
{cycle.total_issues > 0 ? (
|
|
||||||
<>
|
|
||||||
<RadialProgressBar progress={(cycle.completed_issues / cycle.total_issues) * 100} />
|
|
||||||
<span>{Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="normal-case">No issues present</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : cycleStatus === "upcoming" ? (
|
|
||||||
<span className="flex gap-1">
|
|
||||||
<RadialProgressBar progress={100} /> Yet to start
|
|
||||||
</span>
|
|
||||||
) : cycleStatus === "completed" ? (
|
|
||||||
<span className="flex gap-1">
|
|
||||||
<RadialProgressBar progress={100} />
|
|
||||||
<span>{100} %</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="flex gap-1">
|
|
||||||
<RadialProgressBar progress={(cycle.total_issues / cycle.completed_issues) * 100} />
|
|
||||||
{cycleStatus}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* cycle favorite */}
|
|
||||||
{cycle.is_favorite ? (
|
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
|
||||||
<Star className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button type="button" onClick={handleAddToFavorites}>
|
|
||||||
<Star className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<CustomMenu width="auto" verticalEllipsis>
|
|
||||||
{!isCompleted && (
|
|
||||||
<CustomMenu.MenuItem onClick={() => setUpdateModal(true)}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
<span>Edit Cycle</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isCompleted && (
|
|
||||||
<CustomMenu.MenuItem onClick={() => setDeleteModal(true)}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
<span>Delete cycle</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<LinkIcon className="h-4 w-4" />
|
|
||||||
<span>Copy cycle link</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CycleCreateUpdateModal
|
<CycleCreateUpdateModal
|
||||||
data={cycle}
|
data={cycle}
|
||||||
isOpen={updateModal}
|
isOpen={updateModal}
|
||||||
@ -348,7 +142,6 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CycleDeleteModal
|
<CycleDeleteModal
|
||||||
cycle={cycle}
|
cycle={cycle}
|
||||||
isOpen={deleteModal}
|
isOpen={deleteModal}
|
||||||
@ -356,6 +149,110 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||||
|
<a className="group flex items-center justify-between gap-5 px-10 py-6 h-16 w-full text-sm bg-custom-background-100 border-b border-custom-border-100 hover:bg-custom-background-90">
|
||||||
|
<div className="flex items-center gap-3 w-full truncate">
|
||||||
|
<div className="flex items-center gap-4 truncate">
|
||||||
|
<span className="flex-shrink-0">
|
||||||
|
<CircularProgressIndicator size={38} percentage={progress}>
|
||||||
|
{isCompleted ? (
|
||||||
|
<span className="text-sm text-custom-primary-100">{`!`}</span>
|
||||||
|
) : progress === 100 ? (
|
||||||
|
<Check className="h-3 w-3 text-custom-primary-100 stroke-[2]" />
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
|
||||||
|
)}
|
||||||
|
</CircularProgressIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className="flex-shrink-0">
|
||||||
|
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
|
<Tooltip tooltipContent={cycle.name} position="top">
|
||||||
|
<span className="text-base font-medium truncate">{cycle.name}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openCycleOverview} className="flex-shrink-0 hidden group-hover:flex z-10">
|
||||||
|
<Info className="h-4 w-4 text-custom-text-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5 justify-end w-full md:w-auto md:flex-shrink-0 ">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
{currentCycle && (
|
||||||
|
<span
|
||||||
|
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
|
||||||
|
style={{
|
||||||
|
color: currentCycle.color,
|
||||||
|
backgroundColor: `${currentCycle.color}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentCycle.value === "current"
|
||||||
|
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
|
||||||
|
: `${currentCycle.label}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderDate && (
|
||||||
|
<span className="flex items-center justify-center gap-2 w-28 text-xs text-custom-text-300">
|
||||||
|
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
|
||||||
|
{" - "}
|
||||||
|
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
|
||||||
|
<div className="flex items-center justify-center gap-1 cursor-default w-16">
|
||||||
|
{cycle.assignees.length > 0 ? (
|
||||||
|
<AssigneesList users={cycle.assignees} length={2} />
|
||||||
|
) : (
|
||||||
|
<span className="flex items-end justify-center h-5 w-5 bg-custom-background-80 rounded-full border border-dashed border-custom-text-400">
|
||||||
|
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{cycle.is_favorite ? (
|
||||||
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
|
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" onClick={handleAddToFavorites}>
|
||||||
|
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CustomMenu width="auto" ellipsis className="z-10">
|
||||||
|
{!isCompleted && (
|
||||||
|
<>
|
||||||
|
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
<span>Edit cycle</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
<span>Delete module</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<LinkIcon className="h-3 w-3" />
|
||||||
|
<span>Copy cycle link</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// components
|
// components
|
||||||
import { CyclesListItem } from "./cycles-list-item";
|
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -17,18 +18,22 @@ export const CyclesList: FC<ICyclesList> = (props) => {
|
|||||||
const { cycles, filter, workspaceSlug, projectId } = props;
|
const { cycles, filter, workspaceSlug, projectId } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{cycles ? (
|
{cycles ? (
|
||||||
<>
|
<>
|
||||||
{cycles.length > 0 ? (
|
{cycles.length > 0 ? (
|
||||||
<div className="divide-y divide-custom-border-200">
|
<div className="h-full overflow-y-auto">
|
||||||
{cycles.map((cycle) => (
|
<div className="flex justify-between h-full w-full">
|
||||||
<div className="hover:bg-custom-background-80" key={cycle.id}>
|
<div className="flex flex-col h-full w-full overflow-y-auto">
|
||||||
<div className="flex flex-col border-custom-border-200">
|
{cycles.map((cycle) => (
|
||||||
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
<CyclePeekOverview
|
||||||
|
projectId={projectId?.toString() ?? ""}
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full grid place-items-center text-center">
|
<div className="h-full grid place-items-center text-center">
|
||||||
@ -68,6 +73,6 @@ export const CyclesList: FC<ICyclesList> = (props) => {
|
|||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -15,10 +15,11 @@ export interface ICyclesView {
|
|||||||
layout: TCycleLayout;
|
layout: TCycleLayout;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
peekCycle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||||
const { filter, layout, workspaceSlug, projectId } = props;
|
const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
|
||||||
|
|
||||||
// store
|
// store
|
||||||
const { cycle: cycleStore } = useMobxStore();
|
const { cycle: cycleStore } = useMobxStore();
|
||||||
@ -50,7 +51,13 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
|||||||
{layout === "board" && (
|
{layout === "board" && (
|
||||||
<>
|
<>
|
||||||
{!isLoading ? (
|
{!isLoading ? (
|
||||||
<CyclesBoard cycles={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
|
<CyclesBoard
|
||||||
|
cycles={cyclesList}
|
||||||
|
filter={filter}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
peekCycle={peekCycle}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<Loader.Item height="200px" />
|
<Loader.Item height="200px" />
|
||||||
|
@ -17,3 +17,5 @@ export * from "./cycles-board";
|
|||||||
export * from "./cycles-board-card";
|
export * from "./cycles-board-card";
|
||||||
export * from "./cycles-gantt";
|
export * from "./cycles-gantt";
|
||||||
export * from "./delete-modal";
|
export * from "./delete-modal";
|
||||||
|
export * from "./cycle-peek-overview";
|
||||||
|
export * from "./cycles-list-item";
|
||||||
|
@ -15,36 +15,29 @@ import { SidebarProgressStats } from "components/core";
|
|||||||
import ProgressChart from "components/core/sidebar/progress-chart";
|
import ProgressChart from "components/core/sidebar/progress-chart";
|
||||||
import { CycleDeleteModal } from "components/cycles/delete-modal";
|
import { CycleDeleteModal } from "components/cycles/delete-modal";
|
||||||
// ui
|
// ui
|
||||||
import { CustomRangeDatePicker } from "components/ui";
|
import { Avatar, CustomRangeDatePicker } from "components/ui";
|
||||||
import { CustomMenu, Loader, ProgressBar } from "@plane/ui";
|
import { CustomMenu, Loader, LayersIcon } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, MoveRight } from "lucide-react";
|
||||||
CalendarDays,
|
|
||||||
ChevronDown,
|
|
||||||
File,
|
|
||||||
MoveRight,
|
|
||||||
LinkIcon,
|
|
||||||
PieChart,
|
|
||||||
Trash2,
|
|
||||||
UserCircle2,
|
|
||||||
AlertCircle,
|
|
||||||
} from "lucide-react";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
import {
|
import {
|
||||||
|
findHowManyDaysLeft,
|
||||||
getDateRangeStatus,
|
getDateRangeStatus,
|
||||||
isDateGreaterThanToday,
|
isDateGreaterThanToday,
|
||||||
renderDateFormat,
|
renderDateFormat,
|
||||||
renderShortDateWithYearFormat,
|
renderShortDate,
|
||||||
|
renderShortMonthDate,
|
||||||
} from "helpers/date-time.helper";
|
} from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { 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";
|
||||||
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
|
||||||
cycleId: string;
|
cycleId: string;
|
||||||
|
handleClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// services
|
// services
|
||||||
@ -52,12 +45,12 @@ const cycleService = new CycleService();
|
|||||||
|
|
||||||
// TODO: refactor the whole component
|
// TODO: refactor the whole component
|
||||||
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { isOpen, cycleId } = props;
|
const { cycleId, handleClose } = props;
|
||||||
|
|
||||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||||
|
|
||||||
const { user: userStore, cycle: cycleDetailsStore } = useMobxStore();
|
const { user: userStore, cycle: cycleDetailsStore } = useMobxStore();
|
||||||
|
|
||||||
@ -280,6 +273,22 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
if (!cycleDetails) return null;
|
if (!cycleDetails) return null;
|
||||||
|
|
||||||
|
const endDate = new Date(cycleDetails.end_date ?? "");
|
||||||
|
const startDate = new Date(cycleDetails.start_date ?? "");
|
||||||
|
|
||||||
|
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
||||||
|
|
||||||
|
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||||
|
|
||||||
|
const issueCount =
|
||||||
|
cycleDetails.total_issues === 0
|
||||||
|
? "0 Issue"
|
||||||
|
: cycleDetails.total_issues === cycleDetails.completed_issues
|
||||||
|
? cycleDetails.total_issues > 1
|
||||||
|
? `${cycleDetails.total_issues}`
|
||||||
|
: `${cycleDetails.total_issues}`
|
||||||
|
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{cycleDetails && workspaceSlug && projectId && (
|
{cycleDetails && workspaceSlug && projectId && (
|
||||||
@ -291,327 +300,266 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
|
||||||
className={`fixed top-[66px] ${
|
|
||||||
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`}
|
|
||||||
>
|
|
||||||
{cycleDetails ? (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col items-start justify-center">
|
|
||||||
<div className="flex gap-2.5 px-5 text-sm">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="flex items-center rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-center text-xs capitalize">
|
|
||||||
{capitalizeFirstLetter(cycleStatus)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex h-full w-52 items-center gap-2">
|
|
||||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
|
||||||
{({}) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
disabled={isCompleted ?? false}
|
|
||||||
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
|
|
||||||
cycleDetails.start_date ? "" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CalendarDays className="h-3 w-3" />
|
|
||||||
<span>
|
|
||||||
{renderShortDateWithYearFormat(
|
|
||||||
new Date(`${watch("start_date") ? watch("start_date") : cycleDetails?.start_date}`),
|
|
||||||
"Start date"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
{cycleDetails ? (
|
||||||
as={React.Fragment}
|
<>
|
||||||
enter="transition ease-out duration-200"
|
<div className="flex items-center justify-between w-full">
|
||||||
enterFrom="opacity-0 translate-y-1"
|
<div>
|
||||||
enterTo="opacity-100 translate-y-0"
|
{peekCycle && (
|
||||||
leave="transition ease-in duration-150"
|
<button
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
className="flex items-center justify-center h-5 w-5 rounded-full bg-custom-border-300"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
onClick={() => handleClose()}
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
<ChevronRight className="h-3 w-3 text-white stroke-2" />
|
||||||
<CustomRangeDatePicker
|
</button>
|
||||||
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
)}
|
||||||
onChange={(val) => {
|
</div>
|
||||||
if (val) {
|
<div className="flex items-center gap-3.5">
|
||||||
handleStartDateChange(val);
|
<button onClick={handleCopyText}>
|
||||||
}
|
<LinkIcon className="h-3 w-3 text-custom-text-300" />
|
||||||
}}
|
</button>
|
||||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
{!isCompleted && (
|
||||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
<CustomMenu width="lg" ellipsis>
|
||||||
maxDate={new Date(`${watch("end_date")}`)}
|
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
||||||
selectsStart
|
<span className="flex items-center justify-start gap-2">
|
||||||
/>
|
<Trash2 className="h-4 w-4" />
|
||||||
</Popover.Panel>
|
<span>Delete</span>
|
||||||
</Transition>
|
</span>
|
||||||
</>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
</CustomMenu>
|
||||||
</Popover>
|
)}
|
||||||
<span>
|
</div>
|
||||||
<MoveRight className="h-3 w-3 text-custom-text-200" />
|
</div>
|
||||||
</span>
|
|
||||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
|
||||||
{({}) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
disabled={isCompleted ?? false}
|
|
||||||
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
|
|
||||||
cycleDetails.end_date ? "" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CalendarDays className="h-3 w-3" />
|
|
||||||
|
|
||||||
<span>
|
<div className="flex flex-col gap-3">
|
||||||
{renderShortDateWithYearFormat(
|
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">{cycleDetails.name}</h4>
|
||||||
new Date(`${watch("end_date") ? watch("end_date") : cycleDetails?.end_date}`),
|
<div className="flex items-center gap-5">
|
||||||
"End date"
|
{currentCycle && (
|
||||||
)}
|
<span
|
||||||
</span>
|
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
|
||||||
</Popover.Button>
|
style={{
|
||||||
|
color: currentCycle.color,
|
||||||
|
backgroundColor: `${currentCycle.color}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentCycle.value === "current"
|
||||||
|
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
|
||||||
|
: `${currentCycle.label}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="relative flex h-full w-52 items-center gap-2.5">
|
||||||
|
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||||
|
{({}) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button
|
||||||
|
disabled={isCompleted ?? false}
|
||||||
|
className="text-sm text-custom-text-300 font-medium cursor-default"
|
||||||
|
>
|
||||||
|
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
enter="transition ease-out duration-200"
|
enter="transition ease-out duration-200"
|
||||||
enterFrom="opacity-0 translate-y-1"
|
enterFrom="opacity-0 translate-y-1"
|
||||||
enterTo="opacity-100 translate-y-0"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="transition ease-in duration-150"
|
leave="transition ease-in duration-150"
|
||||||
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 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") : cycleDetails?.end_date}
|
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
handleEndDateChange(val);
|
handleStartDateChange(val);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||||
minDate={new Date(`${watch("start_date")}`)}
|
maxDate={new Date(`${watch("end_date")}`)}
|
||||||
selectsEnd
|
selectsStart
|
||||||
/>
|
/>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
<MoveRight className="h-4 w-4 text-custom-text-300" />
|
||||||
|
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||||
|
{({}) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button
|
||||||
|
disabled={isCompleted ?? false}
|
||||||
|
className="text-sm text-custom-text-300 font-medium cursor-default"
|
||||||
|
>
|
||||||
|
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||||
|
<CustomRangeDatePicker
|
||||||
|
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (val) {
|
||||||
|
handleEndDateChange(val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||||
|
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||||
|
minDate={new Date(`${watch("start_date")}`)}
|
||||||
|
selectsEnd
|
||||||
|
/>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col gap-6 px-6 py-6">
|
{cycleDetails.description && (
|
||||||
<div className="flex w-full flex-col items-start justify-start gap-2">
|
<span className="whitespace-normal text-sm leading-5 py-2.5 text-custom-text-200 break-words w-full">
|
||||||
<div className="flex w-full items-start justify-between gap-2">
|
{cycleDetails.description}
|
||||||
<div className="max-w-[300px]">
|
</span>
|
||||||
<h4 className="text-xl font-semibold text-custom-text-100 break-words w-full">
|
)}
|
||||||
{cycleDetails.name}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<CustomMenu width="lg" ellipsis>
|
|
||||||
{!isCompleted && (
|
|
||||||
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
<span>Delete</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<LinkIcon className="h-4 w-4" />
|
|
||||||
<span>Copy link</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
|
<div className="flex flex-col gap-5 pt-2.5 pb-6">
|
||||||
{cycleDetails.description}
|
<div className="flex items-center justify-start gap-1">
|
||||||
</span>
|
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
|
||||||
</div>
|
<UserCircle2 className="h-4 w-4" />
|
||||||
|
<span className="text-base">Lead</span>
|
||||||
<div className="flex flex-col gap-4 text-sm">
|
</div>
|
||||||
<div className="flex items-center justify-start gap-1">
|
<div className="flex items-center w-1/2 rounded-sm">
|
||||||
<div className="flex w-40 items-center justify-start gap-2 text-custom-text-200">
|
<div className="flex items-center gap-2.5">
|
||||||
<UserCircle2 className="h-5 w-5" />
|
<Avatar user={cycleDetails.owned_by} />
|
||||||
<span>Lead</span>
|
<span className="text-sm text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
{cycleDetails.owned_by.avatar && cycleDetails.owned_by.avatar !== "" ? (
|
|
||||||
<img
|
|
||||||
src={cycleDetails.owned_by.avatar}
|
|
||||||
height={12}
|
|
||||||
width={12}
|
|
||||||
className="rounded-full"
|
|
||||||
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">
|
|
||||||
{cycleDetails.owned_by.display_name.charAt(0)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-start gap-1">
|
|
||||||
<div className="flex w-40 items-center justify-start gap-2 text-custom-text-200">
|
|
||||||
<PieChart className="h-5 w-5" />
|
|
||||||
<span>Progress</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5 text-custom-text-200">
|
|
||||||
<span className="h-4 w-4">
|
|
||||||
<ProgressBar value={cycleDetails.completed_issues} maxValue={cycleDetails.total_issues} />
|
|
||||||
</span>
|
|
||||||
{cycleDetails.completed_issues}/{cycleDetails.total_issues}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 p-6">
|
|
||||||
<Disclosure defaultOpen>
|
<div className="flex items-center justify-start gap-1">
|
||||||
|
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
|
||||||
|
<LayersIcon className="h-4 w-4" />
|
||||||
|
<span className="text-base">Issues</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center w-1/2">
|
||||||
|
<span className="text-sm text-custom-text-300 px-1.5">{issueCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 py-5 px-1.5">
|
||||||
|
<Disclosure>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
|
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
|
||||||
<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 && progressPercentage ? (
|
</div>
|
||||||
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
|
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
{progressPercentage ? (
|
||||||
|
<span className="flex items-center justify-center h-5 w-9 rounded text-xs font-medium text-amber-500 bg-amber-50">
|
||||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
|
{isStartValid && isEndValid ? (
|
||||||
|
<Disclosure.Button className="p-1.5">
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Disclosure.Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<AlertCircle height={14} width={14} className="text-custom-text-200" />
|
||||||
|
<span className="text-xs italic text-custom-text-200">
|
||||||
|
Invalid date. Please enter valid date.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isStartValid && isEndValid ? (
|
|
||||||
<Disclosure.Button>
|
|
||||||
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
|
||||||
</Disclosure.Button>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
|
|
||||||
<span className="text-xs italic text-custom-text-200">
|
|
||||||
{cycleStatus === "upcoming"
|
|
||||||
? "Cycle is yet to start."
|
|
||||||
: "Invalid date. Please enter valid date."}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
{isStartValid && isEndValid ? (
|
<div className="flex flex-col gap-3">
|
||||||
<div className=" h-full w-full py-4">
|
{isStartValid && isEndValid ? (
|
||||||
<div className="flex items-start justify-between gap-4 py-2 text-xs">
|
<div className=" h-full w-full pt-4">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-start gap-4 py-2 text-xs">
|
||||||
<span>
|
<div className="flex items-center gap-3 text-custom-text-100">
|
||||||
<File className="h-3 w-3 text-custom-text-200" />
|
<div className="flex items-center justify-center gap-1">
|
||||||
</span>
|
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
||||||
<span>
|
<span>Ideal</span>
|
||||||
Pending Issues -{" "}
|
</div>
|
||||||
{cycleDetails.total_issues -
|
<div className="flex items-center justify-center gap-1">
|
||||||
(cycleDetails.completed_issues + cycleDetails.cancelled_issues)}
|
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
|
||||||
</span>
|
<span>Current</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="relative h-40 w-80">
|
||||||
<div className="flex items-center gap-3 text-custom-text-100">
|
<ProgressChart
|
||||||
<div className="flex items-center justify-center gap-1">
|
distribution={cycleDetails.distribution.completion_chart}
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
startDate={cycleDetails.start_date ?? ""}
|
||||||
<span>Ideal</span>
|
endDate={cycleDetails.end_date ?? ""}
|
||||||
</div>
|
totalIssues={cycleDetails.total_issues}
|
||||||
<div className="flex items-center justify-center gap-1">
|
/>
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
|
|
||||||
<span>Current</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
) : (
|
||||||
<ProgressChart
|
""
|
||||||
distribution={cycleDetails.distribution.completion_chart}
|
)}
|
||||||
startDate={cycleDetails.start_date ?? ""}
|
{cycleDetails.total_issues > 0 && (
|
||||||
endDate={cycleDetails.end_date ?? ""}
|
<div className="h-full w-full pt-5 border-t border-custom-border-200">
|
||||||
|
<SidebarProgressStats
|
||||||
|
distribution={cycleDetails.distribution}
|
||||||
|
groupedIssues={{
|
||||||
|
backlog: cycleDetails.backlog_issues,
|
||||||
|
unstarted: cycleDetails.unstarted_issues,
|
||||||
|
started: cycleDetails.started_issues,
|
||||||
|
completed: cycleDetails.completed_issues,
|
||||||
|
cancelled: cycleDetails.cancelled_issues,
|
||||||
|
}}
|
||||||
totalIssues={cycleDetails.total_issues}
|
totalIssues={cycleDetails.total_issues}
|
||||||
|
isPeekView={Boolean(peekCycle)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 p-6">
|
|
||||||
<Disclosure defaultOpen>
|
|
||||||
{({ open }) => (
|
|
||||||
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
|
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
|
||||||
<div className="flex items-center justify-start gap-2 text-sm">
|
|
||||||
<span className="font-medium text-custom-text-200">Other Information</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{cycleDetails.total_issues > 0 ? (
|
|
||||||
<Disclosure.Button>
|
|
||||||
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
|
||||||
</Disclosure.Button>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
|
|
||||||
<span className="text-xs italic text-custom-text-200">
|
|
||||||
No issues found. Please add issue.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Transition show={open}>
|
|
||||||
<Disclosure.Panel>
|
|
||||||
{cycleDetails.total_issues > 0 ? (
|
|
||||||
<div className="h-full w-full py-4">
|
|
||||||
<SidebarProgressStats
|
|
||||||
distribution={cycleDetails.distribution}
|
|
||||||
groupedIssues={{
|
|
||||||
backlog: cycleDetails.backlog_issues,
|
|
||||||
unstarted: cycleDetails.unstarted_issues,
|
|
||||||
started: cycleDetails.started_issues,
|
|
||||||
completed: cycleDetails.completed_issues,
|
|
||||||
cancelled: cycleDetails.cancelled_issues,
|
|
||||||
}}
|
|
||||||
totalIssues={cycleDetails.total_issues}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
</>
|
||||||
<Loader className="px-5">
|
) : (
|
||||||
<div className="space-y-2">
|
<Loader className="px-5">
|
||||||
<Loader.Item height="15px" width="50%" />
|
<div className="space-y-2">
|
||||||
<Loader.Item height="15px" width="30%" />
|
<Loader.Item height="15px" width="50%" />
|
||||||
</div>
|
<Loader.Item height="15px" width="30%" />
|
||||||
<div className="mt-8 space-y-3">
|
</div>
|
||||||
<Loader.Item height="30px" />
|
<div className="mt-8 space-y-3">
|
||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
</div>
|
<Loader.Item height="30px" />
|
||||||
</Loader>
|
</div>
|
||||||
)}
|
</Loader>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -28,8 +28,8 @@ type Props = {
|
|||||||
export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||||
const { module } = props;
|
const { module } = props;
|
||||||
|
|
||||||
const [editModuleModal, setEditModuleModal] = useState(false);
|
const [editModal, setEditModal] = useState(false);
|
||||||
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -38,50 +38,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const { module: moduleStore } = useMobxStore();
|
const { module: moduleStore } = useMobxStore();
|
||||||
|
|
||||||
const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100;
|
const completionPercentage = (module.completed_issues / module.total_issues) * 100;
|
||||||
|
|
||||||
const handleAddToFavorites = () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
moduleStore.addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't add the module to favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveFromFavorites = () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
moduleStore.removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't remove the module from favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyText = () => {
|
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Module link copied to clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModuleOverview = () => {
|
|
||||||
const { query } = router;
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: { ...query, peekModule: module.id },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const endDate = new Date(module.target_date ?? "");
|
const endDate = new Date(module.target_date ?? "");
|
||||||
const startDate = new Date(module.start_date ?? "");
|
const startDate = new Date(module.start_date ?? "");
|
||||||
@ -101,23 +58,86 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
: `${module.completed_issues}/${module.total_issues} Issues`
|
: `${module.completed_issues}/${module.total_issues} Issues`
|
||||||
: "0 Issue";
|
: "0 Issue";
|
||||||
|
|
||||||
|
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
moduleStore.addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Couldn't add the module to favorites. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
moduleStore.removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Couldn't remove the module from favorites. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Module link copied to clipboard.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModuleOverview = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekModule: module.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workspaceSlug && projectId && (
|
{workspaceSlug && projectId && (
|
||||||
<CreateUpdateModuleModal
|
<CreateUpdateModuleModal
|
||||||
isOpen={editModuleModal}
|
isOpen={editModal}
|
||||||
onClose={() => setEditModuleModal(false)}
|
onClose={() => setEditModal(false)}
|
||||||
data={module}
|
data={module}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DeleteModuleModal data={module} isOpen={moduleDeleteModal} onClose={() => setModuleDeleteModal(false)} />
|
<DeleteModuleModal data={module} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
|
||||||
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
|
||||||
<a className="flex flex-col justify-between p-4 h-44 w-full min-w-[250px] text-sm rounded bg-custom-background-100 border border-custom-border-100 hover:shadow-md">
|
<a className="flex flex-col justify-between p-4 h-44 w-full min-w-[250px] text-sm rounded bg-custom-background-100 border border-custom-border-100 hover:shadow-md">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Tooltip tooltipContent={module.name} position="auto">
|
<Tooltip tooltipContent={module.name} position="top">
|
||||||
<span className="text-base font-medium truncate">{module.name}</span>
|
<span className="text-base font-medium truncate">{module.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -128,13 +148,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
{moduleStatus.label}
|
{moduleStatus.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<button onClick={openModuleOverview}>
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
openModuleOverview();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Info className="h-4 w-4 text-custom-text-400" />
|
<Info className="h-4 w-4 text-custom-text-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -184,60 +198,28 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1.5 z-10">
|
<div className="flex items-center gap-1.5 z-10">
|
||||||
{module.is_favorite ? (
|
{module.is_favorite ? (
|
||||||
<button
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
handleRemoveFromFavorites();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button type="button" onClick={handleAddToFavorites}>
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
handleAddToFavorites();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<CustomMenu width="auto" ellipsis className="z-10">
|
<CustomMenu width="auto" ellipsis className="z-10">
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setEditModuleModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
<span>Edit module</span>
|
<span>Edit module</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setModuleDeleteModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
<span>Delete module</span>
|
<span>Delete module</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCopyText();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon className="h-3 w-3" />
|
<LinkIcon className="h-3 w-3" />
|
||||||
<span>Copy module link</span>
|
<span>Copy module link</span>
|
||||||
|
@ -28,8 +28,8 @@ type Props = {
|
|||||||
export const ModuleListItem: React.FC<Props> = observer((props) => {
|
export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||||
const { module } = props;
|
const { module } = props;
|
||||||
|
|
||||||
const [editModuleModal, setEditModuleModal] = useState(false);
|
const [editModal, setEditModal] = useState(false);
|
||||||
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -40,40 +40,6 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100;
|
const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100;
|
||||||
|
|
||||||
const handleAddToFavorites = () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
moduleStore.addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't add the module to favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveFromFavorites = () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
moduleStore.removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't remove the module from favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyText = () => {
|
|
||||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Module link copied to clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const endDate = new Date(module.target_date ?? "");
|
const endDate = new Date(module.target_date ?? "");
|
||||||
const startDate = new Date(module.start_date ?? "");
|
const startDate = new Date(module.start_date ?? "");
|
||||||
|
|
||||||
@ -87,7 +53,61 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const completedModuleCheck = module.status === "completed" && module.total_issues - module.completed_issues;
|
const completedModuleCheck = module.status === "completed" && module.total_issues - module.completed_issues;
|
||||||
|
|
||||||
const openModuleOverview = () => {
|
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
moduleStore.addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Couldn't add the module to favorites. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
moduleStore.removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Couldn't remove the module from favorites. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Module link copied to clipboard.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteModule = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModuleOverview = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
const { query } = router;
|
const { query } = router;
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
@ -100,14 +120,14 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
<>
|
<>
|
||||||
{workspaceSlug && projectId && (
|
{workspaceSlug && projectId && (
|
||||||
<CreateUpdateModuleModal
|
<CreateUpdateModuleModal
|
||||||
isOpen={editModuleModal}
|
isOpen={editModal}
|
||||||
onClose={() => setEditModuleModal(false)}
|
onClose={() => setEditModal(false)}
|
||||||
data={module}
|
data={module}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DeleteModuleModal data={module} isOpen={moduleDeleteModal} onClose={() => setModuleDeleteModal(false)} />
|
<DeleteModuleModal data={module} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
|
||||||
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
|
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
|
||||||
<a className="group flex items-center justify-between gap-5 px-10 py-6 h-16 w-full text-sm bg-custom-background-100 border-b border-custom-border-100 hover:bg-custom-background-90">
|
<a className="group flex items-center justify-between gap-5 px-10 py-6 h-16 w-full text-sm bg-custom-background-100 border-b border-custom-border-100 hover:bg-custom-background-90">
|
||||||
<div className="flex items-center gap-3 w-full truncate">
|
<div className="flex items-center gap-3 w-full truncate">
|
||||||
@ -123,18 +143,11 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</CircularProgressIndicator>
|
</CircularProgressIndicator>
|
||||||
</span>
|
</span>
|
||||||
<Tooltip tooltipContent={module.name} position="auto">
|
<Tooltip tooltipContent={module.name} position="top">
|
||||||
<span className="text-base font-medium truncate">{module.name}</span>
|
<span className="text-base font-medium truncate">{module.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onClick={openModuleOverview} className="flex-shrink-0 hidden group-hover:flex z-10">
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
openModuleOverview();
|
|
||||||
}}
|
|
||||||
className="flex-shrink-0 hidden group-hover:flex z-10"
|
|
||||||
>
|
|
||||||
<Info className="h-4 w-4 text-custom-text-400" />
|
<Info className="h-4 w-4 text-custom-text-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -171,63 +184,29 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{module.is_favorite ? (
|
{module.is_favorite ? (
|
||||||
<button
|
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
handleRemoveFromFavorites();
|
|
||||||
}}
|
|
||||||
className="z-[1]"
|
|
||||||
>
|
|
||||||
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
handleAddToFavorites();
|
|
||||||
}}
|
|
||||||
className="z-[1]"
|
|
||||||
>
|
|
||||||
<Star className="h-3.5 w-3.5 text-custom-text-300" />
|
<Star className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CustomMenu width="auto" verticalEllipsis buttonClassName="z-[1]">
|
<CustomMenu width="auto" verticalEllipsis buttonClassName="z-[1]">
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setEditModuleModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
<span>Edit module</span>
|
<span>Edit module</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setModuleDeleteModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
<span>Delete module</span>
|
<span>Delete module</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCopyText();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon className="h-3 w-3" />
|
<LinkIcon className="h-3 w-3" />
|
||||||
<span>Copy module link</span>
|
<span>Copy module link</span>
|
||||||
|
@ -400,7 +400,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
}}
|
}}
|
||||||
totalIssues={moduleDetails.total_issues}
|
totalIssues={moduleDetails.total_issues}
|
||||||
module={moduleDetails}
|
module={moduleDetails}
|
||||||
isPeekModuleDetails={Boolean(peekModule)}
|
isPeekView={Boolean(peekModule)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -37,3 +37,40 @@ export const CYCLE_VIEWS = [
|
|||||||
icon: <GanttChart className="h-4 w-4" />,
|
icon: <GanttChart className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const CYCLE_STATUS: {
|
||||||
|
label: string;
|
||||||
|
value: "current" | "upcoming" | "completed" | "draft";
|
||||||
|
color: string;
|
||||||
|
textColor: string;
|
||||||
|
bgColor: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
label: "day left",
|
||||||
|
value: "current",
|
||||||
|
color: "#F59E0B",
|
||||||
|
textColor: "text-amber-500",
|
||||||
|
bgColor: "bg-amber-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Yet to start",
|
||||||
|
value: "upcoming",
|
||||||
|
color: "#3F76FF",
|
||||||
|
textColor: "text-blue-500",
|
||||||
|
bgColor: "bg-indigo-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Completed",
|
||||||
|
value: "completed",
|
||||||
|
color: "#16A34A",
|
||||||
|
textColor: "text-green-600",
|
||||||
|
bgColor: "bg-green-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Draft",
|
||||||
|
value: "draft",
|
||||||
|
color: "#525252",
|
||||||
|
textColor: "text-custom-text-300",
|
||||||
|
bgColor: "bg-custom-background-90",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -25,7 +25,7 @@ const SingleCycle: React.FC = () => {
|
|||||||
|
|
||||||
const { cycle: cycleStore } = useMobxStore();
|
const { cycle: cycleStore } = useMobxStore();
|
||||||
|
|
||||||
const { storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||||
|
|
||||||
const { error } = useSWR(
|
const { error } = useSWR(
|
||||||
@ -35,6 +35,10 @@ const SingleCycle: React.FC = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setValue(`${!isSidebarCollapsed}`);
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: add this function to bulk add issues to cycle
|
// TODO: add this function to bulk add issues to cycle
|
||||||
// const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
// const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
||||||
// if (!workspaceSlug || !projectId) return;
|
// if (!workspaceSlug || !projectId) return;
|
||||||
@ -75,11 +79,21 @@ const SingleCycle: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="relative w-full h-full flex overflow-auto">
|
<div className="flex h-full w-full">
|
||||||
<div className={`h-full w-full ${isSidebarCollapsed ? "" : "mr-[24rem]"} duration-300`}>
|
<div className="h-full w-full">
|
||||||
<CycleLayoutRoot />
|
<CycleLayoutRoot />
|
||||||
</div>
|
</div>
|
||||||
{cycleId && <CycleDetailsSidebar isOpen={!isSidebarCollapsed} cycleId={cycleId.toString()} />}
|
{cycleId && !isSidebarCollapsed && (
|
||||||
|
<div
|
||||||
|
className="flex flex-col gap-3.5 h-full w-[24rem] z-10 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 flex-shrink-0"
|
||||||
|
style={{
|
||||||
|
boxShadow:
|
||||||
|
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CycleDetailsSidebar cycleId={cycleId.toString()} handleClose={toggleSidebar} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -29,7 +29,11 @@ const ProjectCyclesPage: NextPage = observer(() => {
|
|||||||
const { project: projectStore, cycle: cycleStore } = useMobxStore();
|
const { project: projectStore, cycle: cycleStore } = useMobxStore();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
const { workspaceSlug, projectId, peekCycle } = router.query as {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
peekCycle: string;
|
||||||
|
};
|
||||||
// fetching project details
|
// fetching project details
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
||||||
@ -150,13 +154,14 @@ const ProjectCyclesPage: NextPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tab.Panels as={Fragment}>
|
<Tab.Panels as={Fragment}>
|
||||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"all"}
|
filter={"all"}
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
peekCycle={peekCycle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
@ -165,35 +170,38 @@ const ProjectCyclesPage: NextPage = observer(() => {
|
|||||||
<ActiveCycleDetails workspaceSlug={workspaceSlug} projectId={projectId} />
|
<ActiveCycleDetails workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|
||||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"upcoming"}
|
filter={"upcoming"}
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
peekCycle={peekCycle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|
||||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"completed"}
|
filter={"completed"}
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
peekCycle={peekCycle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|
||||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
||||||
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
{cycleView && cycleLayout && workspaceSlug && projectId && (
|
||||||
<CyclesView
|
<CyclesView
|
||||||
filter={"draft"}
|
filter={"draft"}
|
||||||
layout={cycleLayout as TCycleLayout}
|
layout={cycleLayout as TCycleLayout}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
peekCycle={peekCycle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
@ -2755,7 +2755,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-color@^3.0.6":
|
"@types/react-color@^3.0.6", "@types/react-color@^3.0.9":
|
||||||
version "3.0.9"
|
version "3.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.9.tgz#8dbb0d798f2979c3d7e2e26dd46321e80da950b4"
|
resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.9.tgz#8dbb0d798f2979c3d7e2e26dd46321e80da950b4"
|
||||||
integrity sha512-Ojyc6jySSKvM6UYQrZxaYe0JZXtgHHXwR2q9H4MhcNCswFdeZH1owYZCvPtdHtMOfh7t8h1fY0Gd0nvU1JGDkQ==
|
integrity sha512-Ojyc6jySSKvM6UYQrZxaYe0JZXtgHHXwR2q9H4MhcNCswFdeZH1owYZCvPtdHtMOfh7t8h1fY0Gd0nvU1JGDkQ==
|
||||||
@ -7393,7 +7393,7 @@ react-markdown@^8.0.7:
|
|||||||
unist-util-visit "^4.0.0"
|
unist-util-visit "^4.0.0"
|
||||||
vfile "^5.0.0"
|
vfile "^5.0.0"
|
||||||
|
|
||||||
react-moveable@^0.54.1:
|
react-moveable@^0.54.1, react-moveable@^0.54.2:
|
||||||
version "0.54.2"
|
version "0.54.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.2.tgz#87ce9af3499dc1c8218bce7e174b10264c1bbecf"
|
resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.2.tgz#87ce9af3499dc1c8218bce7e174b10264c1bbecf"
|
||||||
integrity sha512-NGaVLbn0i9pb3+BWSKGWFqI/Mgm4+WMeWHxXXQ4Qi1tHxWCXrUrbGvpxEpt69G/hR7dez+/m68ex+fabjnvcUg==
|
integrity sha512-NGaVLbn0i9pb3+BWSKGWFqI/Mgm4+WMeWHxXXQ4Qi1tHxWCXrUrbGvpxEpt69G/hR7dez+/m68ex+fabjnvcUg==
|
||||||
|
Loading…
Reference in New Issue
Block a user