mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
1 Commits
chore/anal
...
feat-cycle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750364833b |
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
|
||||
import { format, parseISO } from "date-fns";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Eye, Users } from "lucide-react";
|
||||
import { Eye, Users, ArrowRight, CalendarDays } from "lucide-react";
|
||||
// types
|
||||
import {
|
||||
CYCLE_FAVORITED,
|
||||
@@ -29,6 +30,7 @@ import { generateQueryParams } from "@/helpers/router.helper";
|
||||
import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useTimeZoneConverter } from "@/hooks/use-timezone-converter";
|
||||
// plane web components
|
||||
import { CycleAdditionalActions } from "@/plane-web/components/cycles";
|
||||
|
||||
@@ -55,6 +57,8 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
const { isProjectTimeZoneDifferent, getProjectUTCOffset, renderFormattedDateInUserTimezone } =
|
||||
useTimeZoneConverter(projectId);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -88,6 +92,8 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
|
||||
const showTransferIssues = routerProjectId && transferableIssuesCount > 0 && cycleStatus === "completed";
|
||||
|
||||
const projectUTCOffset = getProjectUTCOffset();
|
||||
|
||||
const isEditingAllowed = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
@@ -189,14 +195,12 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
<Eye className="h-4 w-4 my-auto text-custom-primary-200" />
|
||||
<span>{t("project_cycles.more_details")}</span>
|
||||
</button>
|
||||
|
||||
{showIssueCount && (
|
||||
<div className="flex items-center gap-1">
|
||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||
<span className="text-xs text-custom-text-300">{cycleDetails.total_issues}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CycleAdditionalActions cycleId={cycleId} projectId={projectId} />
|
||||
{showTransferIssues && (
|
||||
<div
|
||||
@@ -209,34 +213,74 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
<span>Transfer {transferableIssuesCount} work items</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isActive && cycleDetails.start_date && (
|
||||
<DateRangeDropdown
|
||||
buttonVariant={"transparent-with-text"}
|
||||
buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>div]:hover:bg-transparent`}
|
||||
buttonClassName="p-0"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(cycleDetails.start_date),
|
||||
to: getDate(cycleDetails.end_date),
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
to: "End date",
|
||||
}}
|
||||
showTooltip
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled
|
||||
hideIcon={{
|
||||
from: false,
|
||||
to: false,
|
||||
}}
|
||||
/>
|
||||
{isActive ? (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
{/* Duration */}
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<span className="flex gap-1">
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")}
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
|
||||
</span>
|
||||
}
|
||||
disabled={!isProjectTimeZoneDifferent()}
|
||||
tooltipHeading="In your timezone"
|
||||
>
|
||||
<div className="flex gap-1 text-xs text-custom-text-300 font-medium items-center">
|
||||
<CalendarDays className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{cycleDetails.start_date && <span>{format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}</span>}
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{cycleDetails.end_date && <span>{format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}</span>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{projectUTCOffset && (
|
||||
<span className="rounded-md text-xs px-2 cursor-default py-1 bg-custom-background-80 text-custom-text-300">
|
||||
{projectUTCOffset}
|
||||
</span>
|
||||
)}
|
||||
{/* created by */}
|
||||
{createdByDetails && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
cycleDetails.start_date && (
|
||||
<>
|
||||
<DateRangeDropdown
|
||||
buttonVariant={"transparent-with-text"}
|
||||
buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>div]:hover:bg-transparent`}
|
||||
buttonClassName="p-0"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(cycleDetails.start_date),
|
||||
to: getDate(cycleDetails.end_date),
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
to: "End date",
|
||||
}}
|
||||
showTooltip={isProjectTimeZoneDifferent()}
|
||||
customTooltipHeading="In your timezone"
|
||||
customTooltipContent={
|
||||
<span className="flex gap-1">
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")}
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
|
||||
</span>
|
||||
}
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled
|
||||
hideIcon={{
|
||||
from: false,
|
||||
to: false,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* created by */}
|
||||
{createdByDetails && !isActive && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
||||
|
||||
{!isActive && (
|
||||
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
||||
<div className="flex w-10 cursor-default items-center justify-center">
|
||||
@@ -255,7 +299,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isEditingAllowed && !cycleDetails.archived_at && (
|
||||
<FavoriteStar
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -53,6 +53,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||
// TODO: change this logic once backend fix the response
|
||||
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||
const isCompleted = cycleStatus === "completed";
|
||||
const isActive = cycleStatus === "current";
|
||||
|
||||
const cycleTotalIssues =
|
||||
cycleDetails.backlog_issues +
|
||||
@@ -113,6 +114,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||
cycleId={cycleId}
|
||||
cycleDetails={cycleDetails}
|
||||
parentRef={parentRef}
|
||||
isActive={isActive}
|
||||
/>
|
||||
}
|
||||
quickActionElement={
|
||||
|
||||
@@ -50,6 +50,8 @@ type Props = {
|
||||
};
|
||||
renderByDefault?: boolean;
|
||||
renderPlaceholder?: boolean;
|
||||
customTooltipContent?: React.ReactNode;
|
||||
customTooltipHeading?: string;
|
||||
};
|
||||
|
||||
export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
@@ -78,6 +80,8 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
value,
|
||||
renderByDefault = true,
|
||||
renderPlaceholder = true,
|
||||
customTooltipContent,
|
||||
customTooltipHeading,
|
||||
} = props;
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -147,13 +151,15 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="Date range"
|
||||
tooltipHeading={customTooltipHeading ?? "Date range"}
|
||||
tooltipContent={
|
||||
<>
|
||||
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
|
||||
{" - "}
|
||||
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
|
||||
</>
|
||||
customTooltipContent ?? (
|
||||
<>
|
||||
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
|
||||
{" - "}
|
||||
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
|
||||
</>
|
||||
)
|
||||
}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
|
||||
45
web/core/hooks/use-timezone-converter.tsx
Normal file
45
web/core/hooks/use-timezone-converter.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { format } from "date-fns";
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
|
||||
export const useTimeZoneConverter = (projectId: string) => {
|
||||
const { data: user } = useUser();
|
||||
const { getProjectById } = useProject();
|
||||
const userTimezone = user?.user_timezone;
|
||||
const projectTimezone = getProjectById(projectId)?.timezone;
|
||||
|
||||
return {
|
||||
renderFormattedDateInUserTimezone: (date: string, formatToken: string = "MMM dd, yyyy") => {
|
||||
// return if undefined
|
||||
if (!date || !userTimezone) return;
|
||||
// convert the date to the user's timezone
|
||||
const convertedDate = new Date(date).toLocaleString("en-US", { timeZone: userTimezone });
|
||||
// return the formatted date
|
||||
return format(convertedDate, formatToken);
|
||||
},
|
||||
getProjectUTCOffset: () => {
|
||||
if (!projectTimezone) return;
|
||||
|
||||
// Get date in user's timezone
|
||||
const projectDate = new Date(new Date().toLocaleString("en-US", { timeZone: projectTimezone }));
|
||||
const utcDate = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" }));
|
||||
|
||||
// Calculate offset in minutes
|
||||
const offsetInMinutes = (projectDate.getTime() - utcDate.getTime()) / 60000;
|
||||
|
||||
// return if undefined
|
||||
if (!offsetInMinutes) return;
|
||||
|
||||
// Convert to hours and minutes
|
||||
const hours = Math.floor(Math.abs(offsetInMinutes) / 60);
|
||||
const minutes = Math.abs(offsetInMinutes) % 60;
|
||||
|
||||
// Format as +/-HH:mm
|
||||
const sign = offsetInMinutes >= 0 ? "+" : "-";
|
||||
return `UTC ${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
||||
},
|
||||
isProjectTimeZoneDifferent: () => {
|
||||
if (!projectTimezone || !userTimezone) return false;
|
||||
return projectTimezone !== userTimezone;
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user