[WEB-3681]feat: added user timezone dates for cycle (#6820)
* feat: added user timezone dates for cycle * *chore: added translations *chore: refactored user timezone functions
This commit is contained in:
parent
c125bc54ba
commit
ae6e5a48fa
18 changed files with 247 additions and 40 deletions
|
|
@ -1774,6 +1774,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Odeberte filtry pro zobrazení všech cyklů",
|
"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ů",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Aktivní cyklus",
|
"label": "Aktivní cyklus",
|
||||||
"progress": "Pokrok",
|
"progress": "Pokrok",
|
||||||
|
|
|
||||||
|
|
@ -1747,6 +1747,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Entfernen Sie Filter, um alle Zyklen anzuzeigen",
|
"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",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Aktiver Zyklus",
|
"label": "Aktiver Zyklus",
|
||||||
"progress": "Fortschritt",
|
"progress": "Fortschritt",
|
||||||
|
|
|
||||||
|
|
@ -1606,6 +1606,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Remove the filters to see all cycles",
|
"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",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Active cycle",
|
"label": "Active cycle",
|
||||||
"progress": "Progress",
|
"progress": "Progress",
|
||||||
|
|
|
||||||
|
|
@ -1776,6 +1776,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Elimina los filtros para ver todos los ciclos",
|
"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",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Ciclo activo",
|
"label": "Ciclo activo",
|
||||||
"progress": "Progreso",
|
"progress": "Progreso",
|
||||||
|
|
|
||||||
|
|
@ -1774,6 +1774,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Supprimez les filtres pour voir tous les cycles",
|
"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",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Cycle actif",
|
"label": "Cycle actif",
|
||||||
"progress": "Progression",
|
"progress": "Progression",
|
||||||
|
|
|
||||||
|
|
@ -1772,6 +1772,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Rimuovi i filtri per vedere tutti i cicli",
|
"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",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Ciclo attivo",
|
"label": "Ciclo attivo",
|
||||||
"progress": "Avanzamento",
|
"progress": "Avanzamento",
|
||||||
|
|
|
||||||
|
|
@ -1774,6 +1774,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "すべてのサイクルを表示するにはフィルターを解除してください",
|
"remove_filters_to_see_all_cycles": "すべてのサイクルを表示するにはフィルターを解除してください",
|
||||||
"remove_search_criteria_to_see_all_cycles": "すべてのサイクルを表示するには検索条件を解除してください",
|
"remove_search_criteria_to_see_all_cycles": "すべてのサイクルを表示するには検索条件を解除してください",
|
||||||
"only_completed_cycles_can_be_archived": "完了したサイクルのみアーカイブできます",
|
"only_completed_cycles_can_be_archived": "完了したサイクルのみアーカイブできます",
|
||||||
|
"start_date": "開始日",
|
||||||
|
"end_date": "終了日",
|
||||||
|
"in_your_timezone": "あなたのタイムゾーン",
|
||||||
|
"transfer_work_items": "作業項目を転送 {count}",
|
||||||
|
"date_range": "日付範囲",
|
||||||
|
"add_date": "日付を追加",
|
||||||
"active_cycle": {
|
"active_cycle": {
|
||||||
"label": "アクティブなサイクル",
|
"label": "アクティブなサイクル",
|
||||||
"progress": "進捗",
|
"progress": "進捗",
|
||||||
|
|
|
||||||
|
|
@ -1776,6 +1776,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "모든 주기를 보려면 필터를 제거하세요",
|
"remove_filters_to_see_all_cycles": "모든 주기를 보려면 필터를 제거하세요",
|
||||||
"remove_search_criteria_to_see_all_cycles": "모든 주기를 보려면 검색 기준을 제거하세요",
|
"remove_search_criteria_to_see_all_cycles": "모든 주기를 보려면 검색 기준을 제거하세요",
|
||||||
"only_completed_cycles_can_be_archived": "완료된 주기만 아카이브할 수 있습니다",
|
"only_completed_cycles_can_be_archived": "완료된 주기만 아카이브할 수 있습니다",
|
||||||
|
"start_date": "시작일",
|
||||||
|
"end_date": "종료일",
|
||||||
|
"in_your_timezone": "내 시간대",
|
||||||
|
"transfer_work_items": "{count}개의 작업 항목 이전",
|
||||||
|
"date_range": "날짜 범위",
|
||||||
|
"add_date": "날짜 추가",
|
||||||
"active_cycle": {
|
"active_cycle": {
|
||||||
"label": "활성 주기",
|
"label": "활성 주기",
|
||||||
"progress": "진행",
|
"progress": "진행",
|
||||||
|
|
|
||||||
|
|
@ -1747,6 +1747,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Usuń filtry, aby wyświetlić wszystkie cykle",
|
"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",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Aktywny cykl",
|
"label": "Aktywny cykl",
|
||||||
"progress": "Postęp",
|
"progress": "Postęp",
|
||||||
|
|
|
||||||
|
|
@ -1774,6 +1774,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Снимите фильтры для просмотра всех циклов",
|
"remove_filters_to_see_all_cycles": "Снимите фильтры для просмотра всех циклов",
|
||||||
"remove_search_criteria_to_see_all_cycles": "Очистите поиск для просмотра всех циклов",
|
"remove_search_criteria_to_see_all_cycles": "Очистите поиск для просмотра всех циклов",
|
||||||
"only_completed_cycles_can_be_archived": "Только завершённые циклы можно архивировать",
|
"only_completed_cycles_can_be_archived": "Только завершённые циклы можно архивировать",
|
||||||
|
"start_date": "Дата начала",
|
||||||
|
"end_date": "Дата окончания",
|
||||||
|
"in_your_timezone": "В вашем часовом поясе",
|
||||||
|
"transfer_work_items": "Перенести {count} рабочих элементов",
|
||||||
|
"date_range": "Диапазон дат",
|
||||||
|
"add_date": "Добавить дату",
|
||||||
"active_cycle": {
|
"active_cycle": {
|
||||||
"label": "Активный цикл",
|
"label": "Активный цикл",
|
||||||
"progress": "Прогресс",
|
"progress": "Прогресс",
|
||||||
|
|
|
||||||
|
|
@ -1773,6 +1773,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Odstráňte filtre pre zobrazenie všetkých cyklov",
|
"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",
|
"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",
|
"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": {
|
"active_cycle": {
|
||||||
"label": "Aktívny cyklus",
|
"label": "Aktívny cyklus",
|
||||||
"progress": "Pokrok",
|
"progress": "Pokrok",
|
||||||
|
|
|
||||||
|
|
@ -1747,6 +1747,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "Приберіть фільтри, щоб побачити всі цикли",
|
"remove_filters_to_see_all_cycles": "Приберіть фільтри, щоб побачити всі цикли",
|
||||||
"remove_search_criteria_to_see_all_cycles": "Приберіть критерії пошуку, щоб побачити всі цикли",
|
"remove_search_criteria_to_see_all_cycles": "Приберіть критерії пошуку, щоб побачити всі цикли",
|
||||||
"only_completed_cycles_can_be_archived": "Архівувати можна лише завершені цикли",
|
"only_completed_cycles_can_be_archived": "Архівувати можна лише завершені цикли",
|
||||||
|
"start_date": "Дата початку",
|
||||||
|
"end_date": "Дата завершення",
|
||||||
|
"in_your_timezone": "У вашому часовому поясі",
|
||||||
|
"transfer_work_items": "Перенести {count} робочих одиниць",
|
||||||
|
"date_range": "Діапазон дат",
|
||||||
|
"add_date": "Додати дату",
|
||||||
"active_cycle": {
|
"active_cycle": {
|
||||||
"label": "Активний цикл",
|
"label": "Активний цикл",
|
||||||
"progress": "Прогрес",
|
"progress": "Прогрес",
|
||||||
|
|
|
||||||
|
|
@ -1774,6 +1774,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "移除筛选器以查看所有周期",
|
"remove_filters_to_see_all_cycles": "移除筛选器以查看所有周期",
|
||||||
"remove_search_criteria_to_see_all_cycles": "移除搜索条件以查看所有周期",
|
"remove_search_criteria_to_see_all_cycles": "移除搜索条件以查看所有周期",
|
||||||
"only_completed_cycles_can_be_archived": "只能归档已完成的周期",
|
"only_completed_cycles_can_be_archived": "只能归档已完成的周期",
|
||||||
|
"start_date": "开始日期",
|
||||||
|
"end_date": "结束日期",
|
||||||
|
"in_your_timezone": "在您的时区",
|
||||||
|
"transfer_work_items": "转移 {count} 工作项",
|
||||||
|
"date_range": "日期范围",
|
||||||
|
"add_date": "添加日期",
|
||||||
"active_cycle": {
|
"active_cycle": {
|
||||||
"label": "活动周期",
|
"label": "活动周期",
|
||||||
"progress": "进度",
|
"progress": "进度",
|
||||||
|
|
|
||||||
|
|
@ -1776,6 +1776,12 @@
|
||||||
"remove_filters_to_see_all_cycles": "移除篩選器以檢視所有週期",
|
"remove_filters_to_see_all_cycles": "移除篩選器以檢視所有週期",
|
||||||
"remove_search_criteria_to_see_all_cycles": "移除搜尋條件以檢視所有週期",
|
"remove_search_criteria_to_see_all_cycles": "移除搜尋條件以檢視所有週期",
|
||||||
"only_completed_cycles_can_be_archived": "只有已完成的週期可以封存",
|
"only_completed_cycles_can_be_archived": "只有已完成的週期可以封存",
|
||||||
|
"start_date": "開始日期",
|
||||||
|
"end_date": "結束日期",
|
||||||
|
"in_your_timezone": "在您的時區",
|
||||||
|
"transfer_work_items": "轉移 {count} 工作事項",
|
||||||
|
"date_range": "日期範圍",
|
||||||
|
"add_date": "新增日期",
|
||||||
"active_cycle": {
|
"active_cycle": {
|
||||||
"label": "使用中的週期",
|
"label": "使用中的週期",
|
||||||
"progress": "進度",
|
"progress": "進度",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
|
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
|
||||||
|
import { format, parseISO } from "date-fns";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Eye, Users } from "lucide-react";
|
import { Eye, Users, ArrowRight, CalendarDays } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
CYCLE_FAVORITED,
|
CYCLE_FAVORITED,
|
||||||
|
|
@ -29,6 +30,7 @@ import { generateQueryParams } from "@/helpers/router.helper";
|
||||||
import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
import { useTimeZoneConverter } from "@/hooks/use-timezone-converter";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { CycleAdditionalActions } from "@/plane-web/components/cycles";
|
import { CycleAdditionalActions } from "@/plane-web/components/cycles";
|
||||||
|
|
||||||
|
|
@ -55,6 +57,8 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||||
// hooks
|
// hooks
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isProjectTimeZoneDifferent, getProjectUTCOffset, renderFormattedDateInUserTimezone } =
|
||||||
|
useTimeZoneConverter(projectId);
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
@ -88,6 +92,8 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||||
|
|
||||||
const showTransferIssues = routerProjectId && transferableIssuesCount > 0 && cycleStatus === "completed";
|
const showTransferIssues = routerProjectId && transferableIssuesCount > 0 && cycleStatus === "completed";
|
||||||
|
|
||||||
|
const projectUTCOffset = getProjectUTCOffset();
|
||||||
|
|
||||||
const isEditingAllowed = allowPermissions(
|
const isEditingAllowed = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||||
EUserPermissionsLevel.PROJECT,
|
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" />
|
<Eye className="h-4 w-4 my-auto text-custom-primary-200" />
|
||||||
<span>{t("project_cycles.more_details")}</span>
|
<span>{t("project_cycles.more_details")}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showIssueCount && (
|
{showIssueCount && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||||
<span className="text-xs text-custom-text-300">{cycleDetails.total_issues}</span>
|
<span className="text-xs text-custom-text-300">{cycleDetails.total_issues}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CycleAdditionalActions cycleId={cycleId} projectId={projectId} />
|
<CycleAdditionalActions cycleId={cycleId} projectId={projectId} />
|
||||||
{showTransferIssues && (
|
{showTransferIssues && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -206,37 +210,77 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TransferIcon className="fill-custom-primary-200 w-4" />
|
<TransferIcon className="fill-custom-primary-200 w-4" />
|
||||||
<span>Transfer {transferableIssuesCount} work items</span>
|
<span>{t("project_cycles.transfer_work_items", { count: transferableIssuesCount })}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isActive ? (
|
||||||
{!isActive && cycleDetails.start_date && (
|
<>
|
||||||
<DateRangeDropdown
|
<div className="flex gap-2">
|
||||||
buttonVariant={"transparent-with-text"}
|
{/* Duration */}
|
||||||
buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>div]:hover:bg-transparent`}
|
<Tooltip
|
||||||
buttonClassName="p-0"
|
tooltipContent={
|
||||||
minDate={new Date()}
|
<span className="flex gap-1">
|
||||||
value={{
|
{renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")}
|
||||||
from: getDate(cycleDetails.start_date),
|
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||||
to: getDate(cycleDetails.end_date),
|
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
|
||||||
}}
|
</span>
|
||||||
placeholder={{
|
}
|
||||||
from: "Start date",
|
disabled={!isProjectTimeZoneDifferent()}
|
||||||
to: "End date",
|
tooltipHeading={t("project_cycles.date_range")}
|
||||||
}}
|
>
|
||||||
showTooltip
|
<div className="flex gap-1 text-xs text-custom-text-300 font-medium items-center">
|
||||||
required={cycleDetails.status !== "draft"}
|
<CalendarDays className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||||
disabled
|
{cycleDetails.start_date && <span>{format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}</span>}
|
||||||
hideIcon={{
|
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||||
from: false,
|
{cycleDetails.end_date && <span>{format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}</span>}
|
||||||
to: false,
|
</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: t("project_cycles.start_date"),
|
||||||
|
to: t("project_cycles.end_date"),
|
||||||
|
}}
|
||||||
|
showTooltip={isProjectTimeZoneDifferent()}
|
||||||
|
customTooltipHeading={t("project_cycles.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 */}
|
{/* created by */}
|
||||||
{createdByDetails && !isActive && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
{createdByDetails && !isActive && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
||||||
|
|
||||||
{!isActive && (
|
{!isActive && (
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center">
|
<div className="flex w-10 cursor-default items-center justify-center">
|
||||||
|
|
@ -255,7 +299,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isEditingAllowed && !cycleDetails.archived_at && (
|
{isEditingAllowed && !cycleDetails.archived_at && (
|
||||||
<FavoriteStar
|
<FavoriteStar
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||||
// TODO: change this logic once backend fix the response
|
// TODO: change this logic once backend fix the response
|
||||||
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||||
const isCompleted = cycleStatus === "completed";
|
const isCompleted = cycleStatus === "completed";
|
||||||
|
const isActive = cycleStatus === "current";
|
||||||
|
|
||||||
const cycleTotalIssues =
|
const cycleTotalIssues =
|
||||||
cycleDetails.backlog_issues +
|
cycleDetails.backlog_issues +
|
||||||
|
|
@ -113,6 +114,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||||
cycleId={cycleId}
|
cycleId={cycleId}
|
||||||
cycleDetails={cycleDetails}
|
cycleDetails={cycleDetails}
|
||||||
parentRef={parentRef}
|
parentRef={parentRef}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
quickActionElement={
|
quickActionElement={
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { DateRange, Matcher } from "react-day-picker";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react";
|
import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
|
// plane imports
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
// ui
|
// ui
|
||||||
import { ComboDropDown, Calendar } from "@plane/ui";
|
import { ComboDropDown, Calendar } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -50,9 +52,12 @@ type Props = {
|
||||||
};
|
};
|
||||||
renderByDefault?: boolean;
|
renderByDefault?: boolean;
|
||||||
renderPlaceholder?: boolean;
|
renderPlaceholder?: boolean;
|
||||||
|
customTooltipContent?: React.ReactNode;
|
||||||
|
customTooltipHeading?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateRangeDropdown: React.FC<Props> = (props) => {
|
export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
buttonClassName,
|
buttonClassName,
|
||||||
buttonContainerClassName,
|
buttonContainerClassName,
|
||||||
|
|
@ -69,8 +74,8 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||||
maxDate,
|
maxDate,
|
||||||
onSelect,
|
onSelect,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
from: "Add date",
|
from: t("project_cycles.add_date"),
|
||||||
to: "Add date",
|
to: t("project_cycles.add_date"),
|
||||||
},
|
},
|
||||||
placement,
|
placement,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
|
|
@ -78,6 +83,8 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||||
value,
|
value,
|
||||||
renderByDefault = true,
|
renderByDefault = true,
|
||||||
renderPlaceholder = true,
|
renderPlaceholder = true,
|
||||||
|
customTooltipContent,
|
||||||
|
customTooltipHeading,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -147,13 +154,15 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
className={buttonClassName}
|
className={buttonClassName}
|
||||||
isActive={isOpen}
|
isActive={isOpen}
|
||||||
tooltipHeading="Date range"
|
tooltipHeading={customTooltipHeading ?? t("project_cycles.date_range")}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<>
|
customTooltipContent ?? (
|
||||||
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
|
<>
|
||||||
{" - "}
|
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
|
||||||
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
|
{" - "}
|
||||||
</>
|
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
showTooltip={showTooltip}
|
showTooltip={showTooltip}
|
||||||
variant={buttonVariant}
|
variant={buttonVariant}
|
||||||
|
|
|
||||||
69
web/core/hooks/use-timezone-converter.tsx
Normal file
69
web/core/hooks/use-timezone-converter.tsx
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue