diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index 1315ba75c9e..0e2764ec482 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "Odeberte filtry pro zobrazení všech cyklů", "remove_search_criteria_to_see_all_cycles": "Odeberte kritéria pro zobrazení všech cyklů", "only_completed_cycles_can_be_archived": "Lze archivovat pouze dokončené cykly", + "start_date": "Začátek data", + "end_date": "Konec data", + "in_your_timezone": "V časovém pásmu", + "transfer_work_items": "Převést {count} pracovních položek", + "date_range": "Období data", + "add_date": "Přidat datum", "active_cycle": { "label": "Aktivní cyklus", "progress": "Pokrok", diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 4a3a5c06dad..872fc17462a 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -1747,6 +1747,12 @@ "remove_filters_to_see_all_cycles": "Entfernen Sie Filter, um alle Zyklen anzuzeigen", "remove_search_criteria_to_see_all_cycles": "Entfernen Sie Suchkriterien, um alle Zyklen anzuzeigen", "only_completed_cycles_can_be_archived": "Nur abgeschlossene Zyklen können archiviert werden", + "start_date": "Startdatum", + "end_date": "Enddatum", + "in_your_timezone": "In Ihrer Zeitzone", + "transfer_work_items": "Übertragen von {count} Arbeitselementen", + "date_range": "Datumsbereich", + "add_date": "Datum hinzufügen", "active_cycle": { "label": "Aktiver Zyklus", "progress": "Fortschritt", @@ -2321,4 +2327,4 @@ "label": "{count, plural, one {Modul} few {Module} other {Module}}", "no_module": "Kein Modul" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 6beb2917b5f..3566ae98057 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -1606,6 +1606,12 @@ "remove_filters_to_see_all_cycles": "Remove the filters to see all cycles", "remove_search_criteria_to_see_all_cycles": "Remove the search criteria to see all cycles", "only_completed_cycles_can_be_archived": "Only completed cycles can be archived", + "start_date": "Start date", + "end_date": "End date", + "in_your_timezone": "In your timezone", + "transfer_work_items": "Transfer {count} work items", + "date_range": "Date range", + "add_date": "Add date", "active_cycle": { "label": "Active cycle", "progress": "Progress", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index c349364d4bf..3c30865de29 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -1776,6 +1776,12 @@ "remove_filters_to_see_all_cycles": "Elimina los filtros para ver todos los ciclos", "remove_search_criteria_to_see_all_cycles": "Elimina los criterios de búsqueda para ver todos los ciclos", "only_completed_cycles_can_be_archived": "Solo los ciclos completados pueden ser archivados", + "start_date": "Fecha de inicio", + "end_date": "Fecha de finalización", + "in_your_timezone": "En tu zona horaria", + "transfer_work_items": "Transferir {count} elementos de trabajo", + "date_range": "Rango de fechas", + "add_date": "Agregar fecha", "active_cycle": { "label": "Ciclo activo", "progress": "Progreso", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 651622b9714..1b1332d5f24 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "Supprimez les filtres pour voir tous les cycles", "remove_search_criteria_to_see_all_cycles": "Supprimez les critères de recherche pour voir tous les cycles", "only_completed_cycles_can_be_archived": "Seuls les cycles terminés peuvent être archivés", + "start_date": "Date de début", + "end_date": "Date de fin", + "in_your_timezone": "Dans votre fuseau horaire", + "transfer_work_items": "Transférer {count} éléments de travail", + "date_range": "Plage de dates", + "add_date": "Ajouter une date", "active_cycle": { "label": "Cycle actif", "progress": "Progression", diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index ba40f5b9bd1..c83af70a417 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -1772,6 +1772,12 @@ "remove_filters_to_see_all_cycles": "Rimuovi i filtri per vedere tutti i cicli", "remove_search_criteria_to_see_all_cycles": "Rimuovi i criteri di ricerca per vedere tutti i cicli", "only_completed_cycles_can_be_archived": "Solo i cicli completati possono essere archiviati", + "start_date": "Data di inizio", + "end_date": "Data di fine", + "in_your_timezone": "Nel tuo fuso orario", + "transfer_work_items": "Trasferisci {count} elementi di lavoro", + "date_range": "Intervallo di date", + "add_date": "Aggiungi data", "active_cycle": { "label": "Ciclo attivo", "progress": "Avanzamento", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 8f3c82e0596..cb4a2ac78fd 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "すべてのサイクルを表示するにはフィルターを解除してください", "remove_search_criteria_to_see_all_cycles": "すべてのサイクルを表示するには検索条件を解除してください", "only_completed_cycles_can_be_archived": "完了したサイクルのみアーカイブできます", + "start_date": "開始日", + "end_date": "終了日", + "in_your_timezone": "あなたのタイムゾーン", + "transfer_work_items": "作業項目を転送 {count}", + "date_range": "日付範囲", + "add_date": "日付を追加", "active_cycle": { "label": "アクティブなサイクル", "progress": "進捗", diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index b3b0a29d405..794f97e8411 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -1776,6 +1776,12 @@ "remove_filters_to_see_all_cycles": "모든 주기를 보려면 필터를 제거하세요", "remove_search_criteria_to_see_all_cycles": "모든 주기를 보려면 검색 기준을 제거하세요", "only_completed_cycles_can_be_archived": "완료된 주기만 아카이브할 수 있습니다", + "start_date": "시작일", + "end_date": "종료일", + "in_your_timezone": "내 시간대", + "transfer_work_items": "{count}개의 작업 항목 이전", + "date_range": "날짜 범위", + "add_date": "날짜 추가", "active_cycle": { "label": "활성 주기", "progress": "진행", diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 3cee6585357..98c8713b1ca 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -1747,6 +1747,12 @@ "remove_filters_to_see_all_cycles": "Usuń filtry, aby wyświetlić wszystkie cykle", "remove_search_criteria_to_see_all_cycles": "Usuń kryteria wyszukiwania, aby wyświetlić wszystkie cykle", "only_completed_cycles_can_be_archived": "Można archiwizować tylko ukończone cykle", + "start_date": "Data początku", + "end_date": "Data końca", + "in_your_timezone": "W Twojej strefie czasowej", + "transfer_work_items": "Przenieś {count} elementów pracy", + "date_range": "Zakres dat", + "add_date": "Dodaj datę", "active_cycle": { "label": "Aktywny cykl", "progress": "Postęp", @@ -2321,4 +2327,4 @@ "label": "{count, plural, one {Moduł} few {Moduły} other {Modułów}}", "no_module": "Brak modułu" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index d753b30bac1..52c403d1882 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "Снимите фильтры для просмотра всех циклов", "remove_search_criteria_to_see_all_cycles": "Очистите поиск для просмотра всех циклов", "only_completed_cycles_can_be_archived": "Только завершённые циклы можно архивировать", + "start_date": "Дата начала", + "end_date": "Дата окончания", + "in_your_timezone": "В вашем часовом поясе", + "transfer_work_items": "Перенести {count} рабочих элементов", + "date_range": "Диапазон дат", + "add_date": "Добавить дату", "active_cycle": { "label": "Активный цикл", "progress": "Прогресс", diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index 30a1a4db835..8a4645fd46a 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -1773,6 +1773,12 @@ "remove_filters_to_see_all_cycles": "Odstráňte filtre pre zobrazenie všetkých cyklov", "remove_search_criteria_to_see_all_cycles": "Odstráňte kritériá pre zobrazenie všetkých cyklov", "only_completed_cycles_can_be_archived": "Archivovať je možné iba dokončené cykly", + "start_date": "Dátum začiatku", + "end_date": "Dátum konca", + "in_your_timezone": "Váš časový pásmo", + "transfer_work_items": "Presunúť {count} pracovných položiek", + "date_range": "Dátumový rozsah", + "add_date": "Pridať dátum", "active_cycle": { "label": "Aktívny cyklus", "progress": "Pokrok", diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index ace8349fff3..044cfbadc6a 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -1747,6 +1747,12 @@ "remove_filters_to_see_all_cycles": "Приберіть фільтри, щоб побачити всі цикли", "remove_search_criteria_to_see_all_cycles": "Приберіть критерії пошуку, щоб побачити всі цикли", "only_completed_cycles_can_be_archived": "Архівувати можна лише завершені цикли", + "start_date": "Дата початку", + "end_date": "Дата завершення", + "in_your_timezone": "У вашому часовому поясі", + "transfer_work_items": "Перенести {count} робочих одиниць", + "date_range": "Діапазон дат", + "add_date": "Додати дату", "active_cycle": { "label": "Активний цикл", "progress": "Прогрес", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 5e2725127b5..4620d06177c 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "移除筛选器以查看所有周期", "remove_search_criteria_to_see_all_cycles": "移除搜索条件以查看所有周期", "only_completed_cycles_can_be_archived": "只能归档已完成的周期", + "start_date": "开始日期", + "end_date": "结束日期", + "in_your_timezone": "在您的时区", + "transfer_work_items": "转移 {count} 工作项", + "date_range": "日期范围", + "add_date": "添加日期", "active_cycle": { "label": "活动周期", "progress": "进度", diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index a59d78e4dd1..c371f6fc6a2 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -1776,6 +1776,12 @@ "remove_filters_to_see_all_cycles": "移除篩選器以檢視所有週期", "remove_search_criteria_to_see_all_cycles": "移除搜尋條件以檢視所有週期", "only_completed_cycles_can_be_archived": "只有已完成的週期可以封存", + "start_date": "開始日期", + "end_date": "結束日期", + "in_your_timezone": "在您的時區", + "transfer_work_items": "轉移 {count} 工作事項", + "date_range": "日期範圍", + "add_date": "新增日期", "active_cycle": { "label": "使用中的週期", "progress": "進度", diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index bc627fdf28c..8b346b3cd79 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -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 = 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 = 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 = observer((props) => { {t("project_cycles.more_details")} - {showIssueCount && (
{cycleDetails.total_issues}
)} - {showTransferIssues && (
= observer((props) => { }} > - Transfer {transferableIssuesCount} work items + {t("project_cycles.transfer_work_items", { count: transferableIssuesCount })}
)} - - {!isActive && cycleDetails.start_date && ( - 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 ? ( + <> +
+ {/* Duration */} + + {renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")} + + {renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")} + + } + disabled={!isProjectTimeZoneDifferent()} + tooltipHeading={t("project_cycles.date_range")} + > +
+ + {cycleDetails.start_date && {format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}} + + {cycleDetails.end_date && {format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}} +
+
+ {projectUTCOffset && ( + + {projectUTCOffset} + + )} + {/* created by */} + {createdByDetails && } +
+ + ) : ( + cycleDetails.start_date && ( + <> + div]:hover:bg-transparent`} + buttonClassName="p-0" + minDate={new Date()} + value={{ + from: getDate(cycleDetails.start_date), + to: getDate(cycleDetails.end_date), + }} + placeholder={{ + from: t("project_cycles.start_date"), + to: t("project_cycles.end_date"), + }} + showTooltip={isProjectTimeZoneDifferent()} + customTooltipHeading={t("project_cycles.in_your_timezone")} + customTooltipContent={ + + {renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")} + + {renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")} + + } + required={cycleDetails.status !== "draft"} + disabled + hideIcon={{ + from: false, + to: false, + }} + /> + + ) )} - {/* created by */} {createdByDetails && !isActive && } - {!isActive && (
@@ -255,7 +299,6 @@ export const CycleListItemAction: FC = observer((props) => {
)} - {isEditingAllowed && !cycleDetails.archived_at && ( { diff --git a/web/core/components/cycles/list/cycles-list-item.tsx b/web/core/components/cycles/list/cycles-list-item.tsx index aad2f692292..5fca9a9e7ca 100644 --- a/web/core/components/cycles/list/cycles-list-item.tsx +++ b/web/core/components/cycles/list/cycles-list-item.tsx @@ -53,6 +53,7 @@ export const CyclesListItem: FC = 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 = observer((props) => { cycleId={cycleId} cycleDetails={cycleDetails} parentRef={parentRef} + isActive={isActive} /> } quickActionElement={ diff --git a/web/core/components/dropdowns/date-range.tsx b/web/core/components/dropdowns/date-range.tsx index f33a8f8b7ea..0ab9fd44be3 100644 --- a/web/core/components/dropdowns/date-range.tsx +++ b/web/core/components/dropdowns/date-range.tsx @@ -6,6 +6,8 @@ import { DateRange, Matcher } from "react-day-picker"; import { usePopper } from "react-popper"; import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react"; import { Combobox } from "@headlessui/react"; +// plane imports +import { useTranslation } from "@plane/i18n"; // ui import { ComboDropDown, Calendar } from "@plane/ui"; // helpers @@ -50,9 +52,12 @@ type Props = { }; renderByDefault?: boolean; renderPlaceholder?: boolean; + customTooltipContent?: React.ReactNode; + customTooltipHeading?: string; }; export const DateRangeDropdown: React.FC = (props) => { + const { t } = useTranslation(); const { buttonClassName, buttonContainerClassName, @@ -69,8 +74,8 @@ export const DateRangeDropdown: React.FC = (props) => { maxDate, onSelect, placeholder = { - from: "Add date", - to: "Add date", + from: t("project_cycles.add_date"), + to: t("project_cycles.add_date"), }, placement, showTooltip = false, @@ -78,6 +83,8 @@ export const DateRangeDropdown: React.FC = (props) => { value, renderByDefault = true, renderPlaceholder = true, + customTooltipContent, + customTooltipHeading, } = props; // states const [isOpen, setIsOpen] = useState(false); @@ -147,13 +154,15 @@ export const DateRangeDropdown: React.FC = (props) => { - {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} diff --git a/web/core/hooks/use-timezone-converter.tsx b/web/core/hooks/use-timezone-converter.tsx new file mode 100644 index 00000000000..97646a33977 --- /dev/null +++ b/web/core/hooks/use-timezone-converter.tsx @@ -0,0 +1,69 @@ +import { useCallback } from "react"; +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; + + /** + * Render a date in the user's timezone + * @param date - The date to render + * @param formatToken - The format token to use + * @returns The formatted date + */ + const renderFormattedDateInUserTimezone = useCallback( + (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); + }, + [userTimezone] + ); + + /** + * Get the project's UTC offset + * @returns The project's UTC offset + */ + const getProjectUTCOffset = useCallback(() => { + 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")}`; + }, [projectTimezone]); + + /** + * Check if the project's timezone is different from the user's timezone + * @returns True if the project's timezone is different from the user's timezone, false otherwise + */ + const isProjectTimeZoneDifferent = useCallback(() => { + if (!projectTimezone || !userTimezone) return false; + return projectTimezone !== userTimezone; + }, [projectTimezone, userTimezone]); + + return { + renderFormattedDateInUserTimezone, + getProjectUTCOffset, + isProjectTimeZoneDifferent, + }; +};