[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:
parent
7e62c60748
commit
392a6e0137
3 changed files with 78 additions and 238 deletions
|
|
@ -3,16 +3,7 @@
|
|||
import React, { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArchiveRestoreIcon,
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
ChevronRight,
|
||||
EllipsisIcon,
|
||||
LinkIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { ArchiveIcon, ArchiveRestoreIcon, ChevronRight, EllipsisIcon, LinkIcon, Trash2 } from "lucide-react";
|
||||
// types
|
||||
import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
|
@ -20,7 +11,7 @@ import { ICycle } from "@plane/types";
|
|||
// ui
|
||||
import { CustomMenu, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// components
|
||||
import { DateDropdown } from "@/components/dropdowns";
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper";
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
|
|
@ -63,7 +54,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
const { t } = useTranslation();
|
||||
|
||||
// form info
|
||||
const { control, reset, getValues } = useForm({
|
||||
const { control, reset } = useForm({
|
||||
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;
|
||||
|
||||
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
|
||||
await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
|
||||
.then((res) => {
|
||||
captureCycleEvent({
|
||||
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;
|
||||
|
||||
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({
|
||||
...payload,
|
||||
cycle_id: cycleDetails?.id,
|
||||
cycle_id: cycleDetails.id,
|
||||
});
|
||||
else isDateValid = await dateChecker(payload);
|
||||
|
||||
} else {
|
||||
isDateValid = true;
|
||||
}
|
||||
if (isDateValid) {
|
||||
submitChanges(payload, "date_range");
|
||||
setToast({
|
||||
|
|
@ -177,7 +174,6 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
title: t("project_cycles.action.update.failed.title"),
|
||||
message: t("project_cycles.action.update.error.already_exists"),
|
||||
});
|
||||
reset({ ...cycleDetails });
|
||||
}
|
||||
return isDateValid;
|
||||
};
|
||||
|
|
@ -288,79 +284,41 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
|||
</span>
|
||||
)}
|
||||
</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
|
||||
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 ${!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
|
||||
minDate={getDate(getValues("start_date"))}
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="end_date"
|
||||
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
|
||||
<DateRangeDropdown
|
||||
className="h-7"
|
||||
buttonVariant="border-with-text"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(startDateValue),
|
||||
to: getDate(endDateValue),
|
||||
}}
|
||||
onSelect={async (val) => {
|
||||
const isDateValid = await handleDateChange(val?.from, val?.to);
|
||||
if (isDateValid) {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
}
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
to: "End date",
|
||||
}}
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled={!isEditingAllowed || isArchived || isCompleted}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,31 +3,21 @@
|
|||
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { CalendarCheck2, CalendarClock, Eye, Users } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Eye, Users } from "lucide-react";
|
||||
// types
|
||||
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
FavoriteStar,
|
||||
LayersIcon,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
TransferIcon,
|
||||
setPromiseToast,
|
||||
setToast,
|
||||
} from "@plane/ui";
|
||||
import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
|
||||
import { DateDropdown } from "@/components/dropdowns";
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
// constants
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { generateQueryParams } from "@/helpers/router.helper";
|
||||
|
|
@ -36,11 +26,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
|||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { CycleAdditionalActions } from "@/plane-web/components/cycles";
|
||||
// plane web constants
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
|
||||
const cycleService = new CycleService();
|
||||
type Props = {
|
||||
workspaceSlug: 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;
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -206,10 +150,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
});
|
||||
}, [cycleDetails, reset]);
|
||||
|
||||
const isArchived = Boolean(cycleDetails.archived_at);
|
||||
const isCompleted = cycleStatus === "completed";
|
||||
|
||||
const isDisabled = !isEditingAllowed || isArchived || isCompleted;
|
||||
// handlers
|
||||
const openCycleOverview = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -258,81 +198,27 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!isActive && (
|
||||
<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 ${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}
|
||||
/>
|
||||
)}
|
||||
{!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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type Props = {
|
|||
};
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
onSelect: (range: DateRange | undefined) => void;
|
||||
onSelect?: (range: DateRange | undefined) => void;
|
||||
placeholder?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
|
|
@ -204,11 +204,7 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
|||
classNames={{ root: `p-3 rounded-md` }}
|
||||
selected={dateRange}
|
||||
onSelect={(val) => {
|
||||
onSelect(val);
|
||||
setDateRange({
|
||||
from: val?.from ?? undefined,
|
||||
to: val?.to ?? undefined,
|
||||
});
|
||||
onSelect?.(val);
|
||||
}}
|
||||
mode="range"
|
||||
disabled={disabledDays}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue