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 { ISvgIcons } from "./type";
|
||||
import { ISvgIcons } from "../type";
|
||||
|
||||
export const ContrastIcon: React.FC<ISvgIcons> = ({
|
||||
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 { ISvgIcons } from "./type";
|
||||
import { ISvgIcons } from "../type";
|
||||
|
||||
export const DoubleCircleIcon: React.FC<ISvgIcons> = ({
|
||||
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 "./contrast-icon";
|
||||
export * from "./dice-icon";
|
||||
export * from "./layers-icon";
|
||||
export * from "./photo-filter-icon";
|
||||
@ -7,7 +6,6 @@ export * from "./archive-icon";
|
||||
export * from "./admin-profile-icon";
|
||||
export * from "./create-icon";
|
||||
export * from "./subscribe-icon";
|
||||
export * from "./double-circle-icon";
|
||||
export * from "./external-link-icon";
|
||||
export * from "./copy-icon";
|
||||
export * from "./layer-stack";
|
||||
@ -20,6 +18,7 @@ export * from "./blocked-icon";
|
||||
export * from "./blocker-icon";
|
||||
export * from "./related-icon";
|
||||
export * from "./module";
|
||||
export * from "./cycle";
|
||||
export * from "./github-icon";
|
||||
export * from "./discord-icon";
|
||||
export * from "./transfer-icon";
|
||||
|
@ -36,7 +36,7 @@ type Props = {
|
||||
module?: IModule;
|
||||
roundedTab?: boolean;
|
||||
noBackground?: boolean;
|
||||
isPeekModuleDetails?: boolean;
|
||||
isPeekView?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarProgressStats: React.FC<Props> = ({
|
||||
@ -46,7 +46,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
module,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
isPeekModuleDetails = false,
|
||||
isPeekView = false,
|
||||
}) => {
|
||||
const { filters, setFilters } = useIssuesView();
|
||||
|
||||
@ -154,7 +154,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
{...(!isPeekModuleDetails && {
|
||||
{...(!isPeekView && {
|
||||
onClick: () => {
|
||||
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
|
||||
setFilters({
|
||||
@ -213,7 +213,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
completed={label.completed_issues}
|
||||
total={label.total_issues}
|
||||
{...(!isPeekModuleDetails && {
|
||||
{...(!isPeekView && {
|
||||
onClick: () => {
|
||||
if (filters.labels?.includes(label.label_id ?? ""))
|
||||
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 { useRouter } from "next/router";
|
||||
|
||||
// next imports
|
||||
import Link from "next/link";
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { SingleProgressStats } from "components/core";
|
||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||
// ui
|
||||
import { AssigneesList } from "components/ui/avatar";
|
||||
import { CustomMenu, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
|
||||
import { CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } from "@plane/ui";
|
||||
// icons
|
||||
import {
|
||||
AlarmClock,
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
CalendarDays,
|
||||
ChevronDown,
|
||||
LinkIcon,
|
||||
Pencil,
|
||||
Star,
|
||||
Target,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
|
||||
// helpers
|
||||
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
import {
|
||||
getDateRangeStatus,
|
||||
findHowManyDaysLeft,
|
||||
renderShortDate,
|
||||
renderShortMonthDate,
|
||||
} from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
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",
|
||||
},
|
||||
];
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
|
||||
export interface ICyclesBoardCard {
|
||||
workspaceSlug: string;
|
||||
@ -81,7 +49,34 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const endDate = new Date(cycle.end_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 : "";
|
||||
|
||||
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>) => {
|
||||
e.preventDefault();
|
||||
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 (
|
||||
<div>
|
||||
<CycleCreateUpdateModal
|
||||
@ -152,267 +155,119 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
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}`}>
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-5 w-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>
|
||||
<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">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex-shrink-0">
|
||||
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
|
||||
</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
|
||||
className={`rounded-full px-1.5 py-0.5
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: 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"
|
||||
: ""
|
||||
}`}
|
||||
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
|
||||
style={{
|
||||
color: currentCycle.color,
|
||||
backgroundColor: `${currentCycle.color}20`,
|
||||
}}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<RunningIcon className="h-4 w-4" />
|
||||
{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
|
||||
)}
|
||||
{currentCycle.value === "current"
|
||||
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
|
||||
: `${currentCycle.label}`}
|
||||
</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>
|
||||
|
||||
<div className="flex justify-between items-end">
|
||||
<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>
|
||||
<button onClick={openCycleOverview}>
|
||||
<Info className="h-4 w-4 text-custom-text-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-col rounded-b-[10px]">
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<div
|
||||
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 ${
|
||||
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 className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||
<span className="text-xs text-custom-text-300">{issueCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
{cycle.assignees.length > 0 && (
|
||||
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
|
||||
<div className="flex items-center gap-1 cursor-default">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
@ -2,26 +2,41 @@ import { FC } from "react";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// components
|
||||
import { CyclesBoardCard } from "components/cycles";
|
||||
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
||||
|
||||
export interface ICyclesBoard {
|
||||
cycles: ICycle[];
|
||||
filter: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
peekCycle: string;
|
||||
}
|
||||
|
||||
export const CyclesBoard: FC<ICyclesBoard> = (props) => {
|
||||
const { cycles, filter, workspaceSlug, projectId } = props;
|
||||
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<>
|
||||
{cycles.length > 0 ? (
|
||||
<>
|
||||
{cycles.map((cycle) => (
|
||||
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} />
|
||||
))}
|
||||
</>
|
||||
<div className="h-full w-full">
|
||||
<div className="flex justify-between h-full w-full">
|
||||
<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="space-y-2">
|
||||
@ -50,6 +65,6 @@ export const CyclesBoard: FC<ICyclesBoard> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,29 +1,30 @@
|
||||
import { FC, MouseEvent, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// stores
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
|
||||
import { AssigneesList } from "components/ui";
|
||||
// ui
|
||||
import { CustomMenu, RadialProgressBar, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
|
||||
import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon } from "@plane/ui";
|
||||
// icons
|
||||
import {
|
||||
AlarmClock,
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
CalendarDays,
|
||||
LinkIcon,
|
||||
Pencil,
|
||||
Star,
|
||||
Target,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
|
||||
// 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";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
|
||||
type TCyclesListItem = {
|
||||
cycle: ICycle;
|
||||
@ -35,34 +36,6 @@ type TCyclesListItem = {
|
||||
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) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
@ -78,7 +51,28 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const endDate = new Date(cycle.end_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 : "";
|
||||
|
||||
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>) => {
|
||||
e.preventDefault();
|
||||
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 (
|
||||
<>
|
||||
<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
|
||||
data={cycle}
|
||||
isOpen={updateModal}
|
||||
@ -348,7 +142,6 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<CycleDeleteModal
|
||||
cycle={cycle}
|
||||
isOpen={deleteModal}
|
||||
@ -356,6 +149,110 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
workspaceSlug={workspaceSlug}
|
||||
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";
|
||||
// components
|
||||
import { CyclesListItem } from "./cycles-list-item";
|
||||
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
|
||||
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// types
|
||||
@ -17,18 +18,22 @@ export const CyclesList: FC<ICyclesList> = (props) => {
|
||||
const { cycles, filter, workspaceSlug, projectId } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{cycles ? (
|
||||
<>
|
||||
{cycles.length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-custom-background-80" key={cycle.id}>
|
||||
<div className="flex flex-col border-custom-border-200">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="flex justify-between h-full w-full">
|
||||
<div className="flex flex-col h-full w-full overflow-y-auto">
|
||||
{cycles.map((cycle) => (
|
||||
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<CyclePeekOverview
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -15,10 +15,11 @@ export interface ICyclesView {
|
||||
layout: TCycleLayout;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
peekCycle: string;
|
||||
}
|
||||
|
||||
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
const { filter, layout, workspaceSlug, projectId } = props;
|
||||
const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
|
||||
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
@ -50,7 +51,13 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
{layout === "board" && (
|
||||
<>
|
||||
{!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.Item height="200px" />
|
||||
|
@ -17,3 +17,5 @@ export * from "./cycles-board";
|
||||
export * from "./cycles-board-card";
|
||||
export * from "./cycles-gantt";
|
||||
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 { CycleDeleteModal } from "components/cycles/delete-modal";
|
||||
// ui
|
||||
import { CustomRangeDatePicker } from "components/ui";
|
||||
import { CustomMenu, Loader, ProgressBar } from "@plane/ui";
|
||||
import { Avatar, CustomRangeDatePicker } from "components/ui";
|
||||
import { CustomMenu, Loader, LayersIcon } from "@plane/ui";
|
||||
// icons
|
||||
import {
|
||||
CalendarDays,
|
||||
ChevronDown,
|
||||
File,
|
||||
MoveRight,
|
||||
LinkIcon,
|
||||
PieChart,
|
||||
Trash2,
|
||||
UserCircle2,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, MoveRight } from "lucide-react";
|
||||
// helpers
|
||||
import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper";
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
import {
|
||||
findHowManyDaysLeft,
|
||||
getDateRangeStatus,
|
||||
isDateGreaterThanToday,
|
||||
renderDateFormat,
|
||||
renderShortDateWithYearFormat,
|
||||
renderShortDate,
|
||||
renderShortMonthDate,
|
||||
} from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
cycleId: string;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
// services
|
||||
@ -52,12 +45,12 @@ const cycleService = new CycleService();
|
||||
|
||||
// TODO: refactor the whole component
|
||||
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, cycleId } = props;
|
||||
const { cycleId, handleClose } = props;
|
||||
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||
|
||||
const { user: userStore, cycle: cycleDetailsStore } = useMobxStore();
|
||||
|
||||
@ -280,6 +273,22 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
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 (
|
||||
<>
|
||||
{cycleDetails && workspaceSlug && projectId && (
|
||||
@ -291,327 +300,266 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
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
|
||||
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("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
handleStartDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
maxDate={new Date(`${watch("end_date")}`)}
|
||||
selectsStart
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<span>
|
||||
<MoveRight className="h-3 w-3 text-custom-text-200" />
|
||||
</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" />
|
||||
{cycleDetails ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
{peekCycle && (
|
||||
<button
|
||||
className="flex items-center justify-center h-5 w-5 rounded-full bg-custom-border-300"
|
||||
onClick={() => handleClose()}
|
||||
>
|
||||
<ChevronRight className="h-3 w-3 text-white stroke-2" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3.5">
|
||||
<button onClick={handleCopyText}>
|
||||
<LinkIcon className="h-3 w-3 text-custom-text-300" />
|
||||
</button>
|
||||
{!isCompleted && (
|
||||
<CustomMenu width="lg" ellipsis>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{renderShortDateWithYearFormat(
|
||||
new Date(`${watch("end_date") ? watch("end_date") : cycleDetails?.end_date}`),
|
||||
"End date"
|
||||
)}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">{cycleDetails.name}</h4>
|
||||
<div className="flex items-center gap-5">
|
||||
{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(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
|
||||
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>
|
||||
<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("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
handleStartDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
maxDate={new Date(`${watch("end_date")}`)}
|
||||
selectsStart
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<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 className="flex w-full flex-col gap-6 px-6 py-6">
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<div className="max-w-[300px]">
|
||||
<h4 className="text-xl font-semibold text-custom-text-100 break-words w-full">
|
||||
{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>
|
||||
{cycleDetails.description && (
|
||||
<span className="whitespace-normal text-sm leading-5 py-2.5 text-custom-text-200 break-words w-full">
|
||||
{cycleDetails.description}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
|
||||
{cycleDetails.description}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 text-sm">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-40 items-center justify-start gap-2 text-custom-text-200">
|
||||
<UserCircle2 className="h-5 w-5" />
|
||||
<span>Lead</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 className="flex flex-col gap-5 pt-2.5 pb-6">
|
||||
<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">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<span className="text-base">Lead</span>
|
||||
</div>
|
||||
<div className="flex items-center w-1/2 rounded-sm">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Avatar user={cycleDetails.owned_by} />
|
||||
<span className="text-sm text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
|
||||
</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 }) => (
|
||||
<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">
|
||||
<span className="font-medium text-custom-text-200">Progress</span>
|
||||
{!open && progressPercentage ? (
|
||||
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
|
||||
</div>
|
||||
|
||||
<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}%` : ""}
|
||||
</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>
|
||||
{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>
|
||||
<Transition show={open}>
|
||||
<Disclosure.Panel>
|
||||
{isStartValid && isEndValid ? (
|
||||
<div className=" h-full w-full py-4">
|
||||
<div className="flex items-start justify-between gap-4 py-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>
|
||||
<File className="h-3 w-3 text-custom-text-200" />
|
||||
</span>
|
||||
<span>
|
||||
Pending Issues -{" "}
|
||||
{cycleDetails.total_issues -
|
||||
(cycleDetails.completed_issues + cycleDetails.cancelled_issues)}
|
||||
</span>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isStartValid && isEndValid ? (
|
||||
<div className=" h-full w-full pt-4">
|
||||
<div className="flex items-start gap-4 py-2 text-xs">
|
||||
<div className="flex items-center gap-3 text-custom-text-100">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
||||
<span>Ideal</span>
|
||||
</div>
|
||||
<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 className="flex items-center gap-3 text-custom-text-100">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
||||
<span>Ideal</span>
|
||||
</div>
|
||||
<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 className="relative h-40 w-80">
|
||||
<ProgressChart
|
||||
distribution={cycleDetails.distribution.completion_chart}
|
||||
startDate={cycleDetails.start_date ?? ""}
|
||||
endDate={cycleDetails.end_date ?? ""}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<ProgressChart
|
||||
distribution={cycleDetails.distribution.completion_chart}
|
||||
startDate={cycleDetails.start_date ?? ""}
|
||||
endDate={cycleDetails.end_date ?? ""}
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{cycleDetails.total_issues > 0 && (
|
||||
<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}
|
||||
isPeekView={Boolean(peekCycle)}
|
||||
/>
|
||||
</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>
|
||||
<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>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="px-5">
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="15px" width="50%" />
|
||||
<Loader.Item height="15px" width="30%" />
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="px-5">
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="15px" width="50%" />
|
||||
<Loader.Item height="15px" width="30%" />
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -28,8 +28,8 @@ type Props = {
|
||||
export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
const { module } = props;
|
||||
|
||||
const [editModuleModal, setEditModuleModal] = useState(false);
|
||||
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
|
||||
const [editModal, setEditModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -38,50 +38,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const { module: moduleStore } = useMobxStore();
|
||||
|
||||
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 openModuleOverview = () => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekModule: module.id },
|
||||
});
|
||||
};
|
||||
const completionPercentage = (module.completed_issues / module.total_issues) * 100;
|
||||
|
||||
const endDate = new Date(module.target_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`
|
||||
: "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 (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<CreateUpdateModuleModal
|
||||
isOpen={editModuleModal}
|
||||
onClose={() => setEditModuleModal(false)}
|
||||
isOpen={editModal}
|
||||
onClose={() => setEditModal(false)}
|
||||
data={module}
|
||||
projectId={projectId.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}`}>
|
||||
<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 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>
|
||||
</Tooltip>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -128,13 +148,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
{moduleStatus.label}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openModuleOverview();
|
||||
}}
|
||||
>
|
||||
<button onClick={openModuleOverview}>
|
||||
<Info className="h-4 w-4 text-custom-text-400" />
|
||||
</button>
|
||||
</div>
|
||||
@ -184,60 +198,28 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 z-10">
|
||||
{module.is_favorite ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleRemoveFromFavorites();
|
||||
}}
|
||||
>
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleAddToFavorites();
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setEditModuleModal(true);
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Edit module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setModuleDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||
<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={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyText();
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3 w-3" />
|
||||
<span>Copy module link</span>
|
||||
|
@ -28,8 +28,8 @@ type Props = {
|
||||
export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
const { module } = props;
|
||||
|
||||
const [editModuleModal, setEditModuleModal] = useState(false);
|
||||
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
|
||||
const [editModal, setEditModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
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 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 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 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;
|
||||
|
||||
router.push({
|
||||
@ -100,14 +120,14 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<CreateUpdateModuleModal
|
||||
isOpen={editModuleModal}
|
||||
onClose={() => setEditModuleModal(false)}
|
||||
isOpen={editModal}
|
||||
onClose={() => setEditModal(false)}
|
||||
data={module}
|
||||
projectId={projectId.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}`}>
|
||||
<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">
|
||||
@ -123,18 +143,11 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</CircularProgressIndicator>
|
||||
</span>
|
||||
<Tooltip tooltipContent={module.name} position="auto">
|
||||
<Tooltip tooltipContent={module.name} position="top">
|
||||
<span className="text-base font-medium truncate">{module.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openModuleOverview();
|
||||
}}
|
||||
className="flex-shrink-0 hidden group-hover:flex z-10"
|
||||
>
|
||||
<button onClick={openModuleOverview} className="flex-shrink-0 hidden group-hover:flex z-10">
|
||||
<Info className="h-4 w-4 text-custom-text-400" />
|
||||
</button>
|
||||
</div>
|
||||
@ -171,63 +184,29 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
</Tooltip>
|
||||
|
||||
{module.is_favorite ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleRemoveFromFavorites();
|
||||
}}
|
||||
className="z-[1]"
|
||||
>
|
||||
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
|
||||
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleAddToFavorites();
|
||||
}}
|
||||
className="z-[1]"
|
||||
>
|
||||
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<CustomMenu width="auto" verticalEllipsis buttonClassName="z-[1]">
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setEditModuleModal(true);
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Edit module</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setModuleDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||
<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={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyText();
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3 w-3" />
|
||||
<span>Copy module link</span>
|
||||
|
@ -400,7 +400,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
totalIssues={moduleDetails.total_issues}
|
||||
module={moduleDetails}
|
||||
isPeekModuleDetails={Boolean(peekModule)}
|
||||
isPeekView={Boolean(peekModule)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -37,3 +37,40 @@ export const CYCLE_VIEWS = [
|
||||
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 { storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||
|
||||
const { error } = useSWR(
|
||||
@ -35,6 +35,10 @@ const SingleCycle: React.FC = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setValue(`${!isSidebarCollapsed}`);
|
||||
};
|
||||
|
||||
// TODO: add this function to bulk add issues to cycle
|
||||
// const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
||||
// if (!workspaceSlug || !projectId) return;
|
||||
@ -75,11 +79,21 @@ const SingleCycle: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative w-full h-full flex overflow-auto">
|
||||
<div className={`h-full w-full ${isSidebarCollapsed ? "" : "mr-[24rem]"} duration-300`}>
|
||||
<div className="flex h-full w-full">
|
||||
<div className="h-full w-full">
|
||||
<CycleLayoutRoot />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
@ -29,7 +29,11 @@ const ProjectCyclesPage: NextPage = observer(() => {
|
||||
const { project: projectStore, cycle: cycleStore } = useMobxStore();
|
||||
// router
|
||||
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
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
||||
@ -150,13 +154,14 @@ const ProjectCyclesPage: NextPage = observer(() => {
|
||||
</div>
|
||||
|
||||
<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 && (
|
||||
<CyclesView
|
||||
filter={"all"}
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
@ -165,35 +170,38 @@ const ProjectCyclesPage: NextPage = observer(() => {
|
||||
<ActiveCycleDetails workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</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 && (
|
||||
<CyclesView
|
||||
filter={"upcoming"}
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
)}
|
||||
</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 && (
|
||||
<CyclesView
|
||||
filter={"completed"}
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
)}
|
||||
</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 && (
|
||||
<CyclesView
|
||||
filter={"draft"}
|
||||
layout={cycleLayout as TCycleLayout}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
peekCycle={peekCycle}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
@ -2755,7 +2755,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-color@^3.0.6":
|
||||
"@types/react-color@^3.0.6", "@types/react-color@^3.0.9":
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.9.tgz#8dbb0d798f2979c3d7e2e26dd46321e80da950b4"
|
||||
integrity sha512-Ojyc6jySSKvM6UYQrZxaYe0JZXtgHHXwR2q9H4MhcNCswFdeZH1owYZCvPtdHtMOfh7t8h1fY0Gd0nvU1JGDkQ==
|
||||
@ -7393,7 +7393,7 @@ react-markdown@^8.0.7:
|
||||
unist-util-visit "^4.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"
|
||||
resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.2.tgz#87ce9af3499dc1c8218bce7e174b10264c1bbecf"
|
||||
integrity sha512-NGaVLbn0i9pb3+BWSKGWFqI/Mgm4+WMeWHxXXQ4Qi1tHxWCXrUrbGvpxEpt69G/hR7dez+/m68ex+fabjnvcUg==
|
||||
|
Loading…
Reference in New Issue
Block a user