[WEB-3475] fix: cycle dates dropdown (#6690)

* fix: Handled workspace switcher closing on click

* fix: Cycle date picker

* fix: Made onSelect optional in range range component
This commit is contained in:
Akshita Goyal 2025-03-03 17:39:07 +05:30 committed by GitHub
parent 7e62c60748
commit 392a6e0137
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 238 deletions

View file

@ -3,16 +3,7 @@
import React, { FC, useEffect, useState } from "react"; import React, { FC, useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { import { ArchiveIcon, ArchiveRestoreIcon, ChevronRight, EllipsisIcon, LinkIcon, Trash2 } from "lucide-react";
ArchiveIcon,
ArchiveRestoreIcon,
CalendarCheck2,
CalendarClock,
ChevronRight,
EllipsisIcon,
LinkIcon,
Trash2,
} from "lucide-react";
// types // types
import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
@ -20,7 +11,7 @@ import { ICycle } from "@plane/types";
// ui // ui
import { CustomMenu, setToast, TOAST_TYPE } from "@plane/ui"; import { CustomMenu, setToast, TOAST_TYPE } from "@plane/ui";
// components // components
import { DateDropdown } from "@/components/dropdowns"; import { DateRangeDropdown } from "@/components/dropdowns";
// helpers // helpers
import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"; import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper";
@ -63,7 +54,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
// form info // form info
const { control, reset, getValues } = useForm({ const { control, reset } = useForm({
defaultValues, defaultValues,
}); });
@ -110,10 +101,10 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
}); });
}; };
const submitChanges = (data: Partial<ICycle>, changedProperty: string) => { const submitChanges = async (data: Partial<ICycle>, changedProperty: string) => {
if (!workspaceSlug || !projectId || !cycleDetails.id) return; if (!workspaceSlug || !projectId || !cycleDetails.id) return;
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data) await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
.then((res) => { .then((res) => {
captureCycleEvent({ captureCycleEvent({
eventName: CYCLE_UPDATED, eventName: CYCLE_UPDATED,
@ -154,16 +145,22 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
} }
}; };
const handleDateChange = async (payload: { start_date?: string | null; end_date?: string | null }) => { const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => {
let isDateValid = false; let isDateValid = false;
if (cycleDetails?.start_date && cycleDetails?.end_date) const payload = {
start_date: renderFormattedPayloadDate(startDate) || null,
end_date: renderFormattedPayloadDate(endDate) || null,
};
if (payload?.start_date && payload.end_date) {
isDateValid = await dateChecker({ isDateValid = await dateChecker({
...payload, ...payload,
cycle_id: cycleDetails?.id, cycle_id: cycleDetails.id,
}); });
else isDateValid = await dateChecker(payload); } else {
isDateValid = true;
}
if (isDateValid) { if (isDateValid) {
submitChanges(payload, "date_range"); submitChanges(payload, "date_range");
setToast({ setToast({
@ -177,7 +174,6 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
title: t("project_cycles.action.update.failed.title"), title: t("project_cycles.action.update.failed.title"),
message: t("project_cycles.action.update.error.already_exists"), message: t("project_cycles.action.update.error.already_exists"),
}); });
reset({ ...cycleDetails });
} }
return isDateValid; return isDateValid;
}; };
@ -288,79 +284,41 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
</span> </span>
)} )}
</div> </div>
<div className="flex items-center gap-2">
<Controller
name="start_date"
control={control}
rules={{ required: "Please select a date" }}
render={({ field: { value, onChange } }) => (
<DateDropdown
value={value ?? null}
onChange={async (val) => {
let isDateValid;
const valDate = val ? renderFormattedPayloadDate(val) : null;
if (getValues("end_date")) {
isDateValid = await handleDateChange({
start_date: valDate,
end_date: renderFormattedPayloadDate(getValues("end_date")),
});
} else {
isDateValid = await handleDateChange({
start_date: valDate,
end_date: valDate,
});
}
isDateValid && onChange(renderFormattedPayloadDate(val));
}}
placeholder={t("common.order_by.start_date")}
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant={value ? "border-with-text" : "border-without-text"}
buttonContainerClassName={`h-6 w-full flex ${!isEditingAllowed || isArchived || isCompleted ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 rounded text-xs`}
optionsClassName="z-10"
disabled={!isEditingAllowed || isArchived || isCompleted}
showTooltip
maxDate={getDate(getValues("end_date"))}
isClearable={false}
/>
)}
/>
<Controller <Controller
name="end_date" control={control}
control={control} name="start_date"
rules={{ required: "Please select a date" }} render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
render={({ field: { value, onChange } }) => ( <Controller
<DateDropdown control={control}
value={getDate(value) ?? null} name="end_date"
onChange={async (val) => { render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
let isDateValid; <DateRangeDropdown
const valDate = val ? renderFormattedPayloadDate(val) : null; className="h-7"
if (getValues("start_date")) { buttonVariant="border-with-text"
isDateValid = await handleDateChange({ minDate={new Date()}
end_date: valDate, value={{
start_date: renderFormattedPayloadDate(getValues("start_date")), from: getDate(startDateValue),
}); to: getDate(endDateValue),
} else { }}
isDateValid = await handleDateChange({ onSelect={async (val) => {
end_date: valDate, const isDateValid = await handleDateChange(val?.from, val?.to);
start_date: valDate, if (isDateValid) {
}); onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
} onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
isDateValid && onChange(renderFormattedPayloadDate(val)); }
}} }}
placeholder={t("common.order_by.due_date")} placeholder={{
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />} from: "Start date",
buttonVariant={value ? "border-with-text" : "border-without-text"} to: "End date",
buttonContainerClassName={`h-6 w-full flex ${!isEditingAllowed || isArchived || isCompleted ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 rounded text-xs`} }}
optionsClassName="z-10" required={cycleDetails.status !== "draft"}
disabled={!isEditingAllowed || isArchived || isCompleted} disabled={!isEditingAllowed || isArchived || isCompleted}
showTooltip />
minDate={getDate(getValues("start_date"))} )}
isClearable={false} />
/> )}
)} />
/>
</div>
</div> </div>
</> </>
); );

View file

@ -3,31 +3,21 @@
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react"; import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
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 { Controller, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { CalendarCheck2, CalendarClock, Eye, Users } from "lucide-react"; import { Eye, Users } from "lucide-react";
// types // types
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { ICycle, TCycleGroups } from "@plane/types"; import { ICycle, TCycleGroups } from "@plane/types";
// ui // ui
import { import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, setPromiseToast } from "@plane/ui";
Avatar,
AvatarGroup,
FavoriteStar,
LayersIcon,
TOAST_TYPE,
Tooltip,
TransferIcon,
setPromiseToast,
setToast,
} from "@plane/ui";
// components // components
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles"; import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
import { DateDropdown } from "@/components/dropdowns"; import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
// constants // constants
// helpers // helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper"; import { getFileURL } from "@/helpers/file.helper";
// hooks // hooks
import { generateQueryParams } from "@/helpers/router.helper"; import { generateQueryParams } from "@/helpers/router.helper";
@ -36,11 +26,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components // plane web components
import { CycleAdditionalActions } from "@/plane-web/components/cycles"; import { CycleAdditionalActions } from "@/plane-web/components/cycles";
// plane web constants
// services
import { CycleService } from "@/services/cycle.service";
const cycleService = new CycleService();
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
@ -155,48 +141,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
}); });
}; };
const submitChanges = (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleId) return;
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
};
const dateChecker = async (payload: any) => {
try {
const res = await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload);
return res.status;
} catch {
return false;
}
};
const handleDateChange = async (payload: { start_date?: string | null; end_date?: string | null }) => {
let isDateValid = false;
if (cycleDetails?.start_date && cycleDetails?.end_date)
isDateValid = await dateChecker({
...payload,
cycle_id: cycleDetails?.id,
});
else isDateValid = await dateChecker(payload);
if (isDateValid) {
submitChanges(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("project_cycles.action.update.success.title"),
message: t("project_cycles.action.update.success.description"),
});
} else {
setToast({
type: TOAST_TYPE.ERROR,
title: t("project_cycles.action.update.failed.title"),
message: t("project_cycles.action.update.error.already_exists"),
});
reset({ ...cycleDetails });
}
return isDateValid;
};
const createdByDetails = cycleDetails.created_by ? getUserDetails(cycleDetails.created_by) : undefined; const createdByDetails = cycleDetails.created_by ? getUserDetails(cycleDetails.created_by) : undefined;
useEffect(() => { useEffect(() => {
@ -206,10 +150,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
}); });
}, [cycleDetails, reset]); }, [cycleDetails, reset]);
const isArchived = Boolean(cycleDetails.archived_at);
const isCompleted = cycleStatus === "completed";
const isDisabled = !isEditingAllowed || isArchived || isCompleted;
// handlers // handlers
const openCycleOverview = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => { const openCycleOverview = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
e.preventDefault(); e.preventDefault();
@ -258,81 +198,27 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
</div> </div>
)} )}
{!isActive && ( {!isActive && cycleDetails.start_date && (
<Controller <DateRangeDropdown
name="start_date" buttonVariant={"transparent-with-text"}
control={control} buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>div]:hover:bg-transparent`}
rules={{ required: "Please select a date" }} buttonClassName="p-0"
render={({ field: { value, onChange } }) => ( minDate={new Date()}
<DateDropdown value={{
value={value ?? null} from: getDate(cycleDetails.start_date),
onChange={async (val) => { to: getDate(cycleDetails.end_date),
let isDateValid; }}
const valDate = val ? renderFormattedPayloadDate(val) : null; placeholder={{
if (getValues("end_date")) { from: "Start date",
isDateValid = await handleDateChange({ to: "End date",
start_date: valDate, }}
end_date: renderFormattedPayloadDate(getValues("end_date")), showTooltip
}); required={cycleDetails.status !== "draft"}
} else { disabled
isDateValid = await handleDateChange({ hideIcon={{
start_date: valDate, from: false,
end_date: valDate, to: false,
}); }}
}
isDateValid && onChange(renderFormattedPayloadDate(val));
}}
placeholder={t("common.order_by.start_date")}
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant={value ? "border-with-text" : "border-without-text"}
buttonContainerClassName={`h-6 w-full flex ${isDisabled ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 rounded text-xs`}
optionsClassName="z-10"
disabled={isDisabled}
renderByDefault={isMobile}
showTooltip
maxDate={getDate(getValues("end_date"))}
isClearable={false}
/>
)}
/>
)}
{!isActive && (
<Controller
name="end_date"
control={control}
rules={{ required: "Please select a date" }}
render={({ field: { value, onChange } }) => (
<DateDropdown
value={getDate(value) ?? null}
onChange={async (val) => {
let isDateValid;
const valDate = val ? renderFormattedPayloadDate(val) : null;
if (getValues("start_date")) {
isDateValid = await handleDateChange({
end_date: valDate,
start_date: renderFormattedPayloadDate(getValues("start_date")),
});
} else {
isDateValid = await handleDateChange({
end_date: valDate,
start_date: valDate,
});
}
isDateValid && onChange(renderFormattedPayloadDate(val));
}}
placeholder={t("common.order_by.due_date")}
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
buttonVariant={value ? "border-with-text" : "border-without-text"}
buttonContainerClassName={`h-6 w-full flex ${isDisabled ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 rounded text-xs`}
optionsClassName="z-10"
disabled={isDisabled}
renderByDefault={isMobile}
showTooltip
minDate={getDate(getValues("start_date"))}
isClearable={false}
/>
)}
/> />
)} )}

View file

@ -35,7 +35,7 @@ type Props = {
}; };
minDate?: Date; minDate?: Date;
maxDate?: Date; maxDate?: Date;
onSelect: (range: DateRange | undefined) => void; onSelect?: (range: DateRange | undefined) => void;
placeholder?: { placeholder?: {
from?: string; from?: string;
to?: string; to?: string;
@ -204,11 +204,7 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
classNames={{ root: `p-3 rounded-md` }} classNames={{ root: `p-3 rounded-md` }}
selected={dateRange} selected={dateRange}
onSelect={(val) => { onSelect={(val) => {
onSelect(val); onSelect?.(val);
setDateRange({
from: val?.from ?? undefined,
to: val?.to ?? undefined,
});
}} }}
mode="range" mode="range"
disabled={disabledDays} disabled={disabledDays}