[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 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue