[WEB-371]: Implemented react-day-picker for date selections (#3679)

* dev: initialize new date picker

* style: selected date focus state

* chore: replace custom date filter modal components

* chore: replaced inbox snooze popover datepicker

* chore: replaced the custom date picker

* style: date range picker designed

* chore: date range picker implemented throughout the platform

* chore: updated tab indices

* chore: range-picker in the issue layouts

* chore: passed due date color

* chore: removed range picker from issue dates
This commit is contained in:
Aaryan Khandelwal 2024-02-21 19:55:18 +05:30 committed by GitHub
parent e86d2ba743
commit b1592adc66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1018 additions and 998 deletions

View File

@ -1,11 +1,10 @@
import { useState } from "react";
import { add } from "date-fns";
import { Controller, useForm } from "react-hook-form";
import { DateDropdown } from "components/dropdowns";
import { Calendar } from "lucide-react";
// hooks
import useToast from "hooks/use-toast";
// components
import { CustomDatePicker } from "components/ui";
// ui
import { Button, CustomSelect, Input, TextArea, ToggleSwitch } from "@plane/ui";
// helpers
@ -167,7 +166,7 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
<CustomSelect
customButton={
<div
className={`flex items-center gap-2 rounded border-[0.5px] border-custom-border-200 px-2 py-1 ${
className={`flex items-center gap-2 rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 ${
neverExpires ? "text-custom-text-400" : ""
}`}
>
@ -194,20 +193,13 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
}}
/>
{watch("expired_at") === "custom" && (
<CustomDatePicker
<DateDropdown
value={customDate}
onChange={(date) => setCustomDate(date ? new Date(date) : null)}
onChange={(date) => setCustomDate(date)}
minDate={tomorrow}
customInput={
<div
className={`flex cursor-pointer items-center gap-2 !rounded border-[0.5px] border-custom-border-200 px-2 py-1 text-xs !shadow-none !duration-0 ${
customDate ? "w-[7.5rem]" : ""
} ${neverExpires ? "!cursor-not-allowed text-custom-text-400" : "hover:bg-custom-background-80"}`}
>
<Calendar className="h-3 w-3" />
{customDate ? renderFormattedDate(customDate) : "Set date"}
</div>
}
icon={<Calendar className="h-3 w-3" />}
buttonVariant="border-with-text"
placeholder="Set date"
disabled={neverExpires}
/>
)}

View File

@ -1,13 +1,12 @@
import { Fragment } from "react";
import { Controller, useForm } from "react-hook-form";
import DatePicker from "react-datepicker";
import { DayPicker } from "react-day-picker";
import { Dialog, Transition } from "@headlessui/react";
import { X } from "lucide-react";
// components
import { DateFilterSelect } from "./date-filter-select";
// ui
import { Button } from "@plane/ui";
// icons
import { X } from "lucide-react";
// helpers
import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper";
@ -46,9 +45,6 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
const nextDay = new Date(watch("date1"));
nextDay.setDate(nextDay.getDate() + 1);
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -91,12 +87,15 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
control={control}
name="date1"
render={({ field: { value, onChange } }) => (
<DatePicker
selected={value}
onChange={(val) => onChange(val)}
dateFormat="dd-MM-yyyy"
calendarClassName="h-full"
inline
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => onChange(date)}
mode="single"
disabled={[
{ after: new Date(watch("date2")) }
]}
className="border border-custom-border-200 p-3 rounded-md"
/>
)}
/>
@ -105,13 +104,15 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
control={control}
name="date2"
render={({ field: { value, onChange } }) => (
<DatePicker
selected={value}
onChange={onChange}
dateFormat="dd-MM-yyyy"
calendarClassName="h-full"
minDate={nextDay}
inline
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => onChange(date)}
mode="single"
disabled={[
{ before: new Date(watch("date1")) }
]}
className="border border-custom-border-200 p-3 rounded-md"
/>
)}
/>

View File

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// components
import { DateDropdown, ProjectDropdown } from "components/dropdowns";
import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns";
// ui
import { Button, Input, TextArea } from "@plane/ui";
// helpers
@ -32,7 +32,6 @@ export const CycleForm: React.FC<Props> = (props) => {
formState: { errors, isSubmitting, dirtyFields },
handleSubmit,
control,
watch,
reset,
} = useForm<ICycle>({
defaultValues: {
@ -51,15 +50,6 @@ export const CycleForm: React.FC<Props> = (props) => {
});
}, [data, reset]);
const startDate = watch("start_date");
const endDate = watch("end_date");
const minDate = startDate ? new Date(startDate) : new Date();
minDate.setDate(minDate.getDate() + 1);
const maxDate = endDate ? new Date(endDate) : null;
maxDate?.setDate(maxDate.getDate() - 1);
return (
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData, dirtyFields))}>
<div className="space-y-5">
@ -132,39 +122,37 @@ export const CycleForm: React.FC<Props> = (props) => {
</div>
<div className="flex flex-wrap items-center gap-2">
<div>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Start date"
minDate={new Date()}
maxDate={maxDate ?? undefined}
tabIndex={3}
/>
</div>
)}
/>
</div>
<Controller
control={control}
name="end_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="End date"
minDate={minDate}
tabIndex={4}
/>
</div>
name="start_date"
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
<Controller
control={control}
name="end_date"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown
buttonVariant="border-with-text"
className="h-7"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
}}
placeholder={{
from: "Start date",
to: "End date",
}}
hideIcon={{
to: true,
}}
tabIndex={3}
/>
)}
/>
)}
/>
</div>
@ -172,10 +160,10 @@ export const CycleForm: React.FC<Props> = (props) => {
</div>
</div>
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={5}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={6}>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
{data ? (isSubmitting ? "Updating" : "Update cycle") : isSubmitting ? "Creating" : "Create cycle"}
</Button>
</div>

View File

@ -1,8 +1,8 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
import { Controller, useForm } from "react-hook-form";
import { Disclosure, Transition } from "@headlessui/react";
import isEmpty from "lodash/isEmpty";
// services
import { CycleService } from "services/cycle.service";
@ -14,27 +14,12 @@ import { SidebarProgressStats } from "components/core";
import ProgressChart from "components/core/sidebar/progress-chart";
import { CycleDeleteModal } from "components/cycles/delete-modal";
// ui
import { CustomRangeDatePicker } from "components/ui";
import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui";
// icons
import {
ChevronDown,
LinkIcon,
Trash2,
UserCircle2,
AlertCircle,
ChevronRight,
CalendarCheck2,
CalendarClock,
} from "lucide-react";
import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
import {
findHowManyDaysLeft,
isDateGreaterThanToday,
renderFormattedPayloadDate,
renderFormattedDate,
} from "helpers/date-time.helper";
import { findHowManyDaysLeft, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { ICycle } from "@plane/types";
// constants
@ -42,6 +27,7 @@ import { EUserWorkspaceRoles } from "constants/workspace";
import { CYCLE_UPDATED } from "constants/event-tracker";
// fetch-keys
import { CYCLE_STATUS } from "constants/cycle";
import { DateRangeDropdown } from "components/dropdowns";
type Props = {
cycleId: string;
@ -61,9 +47,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { cycleId, handleClose } = props;
// states
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// refs
const startDateButtonRef = useRef<HTMLButtonElement | null>(null);
const endDateButtonRef = useRef<HTMLButtonElement | null>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
@ -74,13 +57,13 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
} = useUser();
const { getCycleById, updateCycleDetails } = useCycle();
const { getUserDetails } = useMember();
// derived values
const cycleDetails = getCycleById(cycleId);
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined;
// toast alert
const { setToastAlert } = useToast();
const { setValue, reset, watch } = useForm({
// form info
const { control, reset } = useForm({
defaultValues,
});
@ -145,160 +128,38 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}
};
const handleStartDateChange = async (date: string) => {
setValue("start_date", date);
const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => {
if (!startDate || !endDate) return;
if (!watch("end_date") || watch("end_date") === "") endDateButtonRef.current?.click();
let isDateValid = false;
if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") {
if (!isDateGreaterThanToday(`${watch("end_date")}`)) {
setToastAlert({
type: "error",
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
});
reset({ ...cycleDetails });
return;
}
const payload = {
start_date: renderFormattedPayloadDate(startDate),
end_date: renderFormattedPayloadDate(endDate),
};
if (cycleDetails?.start_date && cycleDetails?.end_date) {
const isDateValidForExistingCycle = await dateChecker({
start_date: `${watch("start_date")}`,
end_date: `${watch("end_date")}`,
cycle_id: cycleDetails.id,
});
if (isDateValidForExistingCycle) {
submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
},
"start_date"
);
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle updated successfully.",
});
} else {
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
reset({ ...cycleDetails });
return;
}
const isDateValid = await dateChecker({
start_date: `${watch("start_date")}`,
end_date: `${watch("end_date")}`,
if (cycleDetails && cycleDetails.start_date && cycleDetails.end_date)
isDateValid = await dateChecker({
...payload,
cycle_id: cycleDetails.id,
});
else isDateValid = await dateChecker(payload);
if (isDateValid) {
submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
},
"start_date"
);
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle updated successfully.",
});
} else {
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
reset({ ...cycleDetails });
}
}
};
const handleEndDateChange = async (date: string) => {
setValue("end_date", date);
if (!watch("start_date") || watch("start_date") === "") startDateButtonRef.current?.click();
if (watch("start_date") && watch("end_date") && watch("start_date") !== "" && watch("start_date") !== "") {
if (!isDateGreaterThanToday(`${watch("end_date")}`)) {
setToastAlert({
type: "error",
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
});
reset({ ...cycleDetails });
return;
}
if (cycleDetails?.start_date && cycleDetails?.end_date) {
const isDateValidForExistingCycle = await dateChecker({
start_date: `${watch("start_date")}`,
end_date: `${watch("end_date")}`,
cycle_id: cycleDetails.id,
});
if (isDateValidForExistingCycle) {
submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
},
"end_date"
);
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle updated successfully.",
});
} else {
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
reset({ ...cycleDetails });
return;
}
const isDateValid = await dateChecker({
start_date: `${watch("start_date")}`,
end_date: `${watch("end_date")}`,
if (isDateValid) {
submitChanges(payload, "date_range");
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle updated successfully.",
});
if (isDateValid) {
submitChanges(
{
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
end_date: renderFormattedPayloadDate(`${watch("end_date")}`),
},
"end_date"
);
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle updated successfully.",
});
} else {
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
reset({ ...cycleDetails });
}
} else {
setToastAlert({
type: "error",
title: "Error!",
message:
"You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.",
});
reset({ ...cycleDetails });
}
};
@ -351,9 +212,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</Loader>
);
const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? "");
const startDate = new Date(watch("start_date") ?? cycleDetails.start_date ?? "");
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const issueCount =
@ -440,125 +298,52 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex flex-col gap-5 pb-6 pt-2.5">
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<CalendarClock className="h-4 w-4" />
<span className="text-base">Start date</span>
<span className="text-base">Date range</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={startDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("start_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(startDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON");
handleStartDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("end_date")}`)}
selectsStart={watch("end_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
<div className="w-3/5 h-7">
<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"
buttonContainerClassName="w-full"
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}}
placeholder={{
from: "Start date",
to: "End date",
}}
required={cycleDetails.status !== "draft"}
/>
)}
/>
)}
</Popover>
/>
</div>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<CalendarCheck2 className="h-4 w-4" />
<span className="text-base">Target date</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={endDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("end_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(endDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON");
handleEndDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<UserCircle2 className="h-4 w-4" />
<span className="text-base">Lead</span>
</div>
<div className="flex w-1/2 items-center rounded-sm">
<div className="flex w-3/5 items-center rounded-sm">
<div className="flex items-center gap-2.5">
<Avatar name={cycleOwnerDetails?.display_name} src={cycleOwnerDetails?.avatar} />
<span className="text-sm text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
@ -567,11 +352,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<LayersIcon className="h-4 w-4" />
<span className="text-base">Issues</span>
</div>
<div className="flex w-1/2 items-center">
<div className="flex w-3/5 items-center">
<span className="px-1.5 text-sm text-custom-text-300">{issueCount}</span>
</div>
</div>

View File

@ -0,0 +1,261 @@
import React, { useEffect, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { DateRange, DayPicker, Matcher } from "react-day-picker";
import { ArrowRight, CalendarDays } from "lucide-react";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
// components
import { DropdownButton } from "./buttons";
// ui
import { Button } from "@plane/ui";
// helpers
import { cn } from "helpers/common.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { TButtonVariants } from "./types";
type Props = {
applyButtonText?: string;
bothRequired?: boolean;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonFromDateClassName?: string;
buttonToDateClassName?: string;
buttonVariant: TButtonVariants;
cancelButtonText?: string;
className?: string;
disabled?: boolean;
hideIcon?: {
from?: boolean;
to?: boolean;
};
icon?: React.ReactNode;
minDate?: Date;
maxDate?: Date;
onSelect: (range: DateRange | undefined) => void;
placeholder?: {
from?: string;
to?: string;
};
placement?: Placement;
required?: boolean;
showTooltip?: boolean;
tabIndex?: number;
value: {
from: Date | undefined;
to: Date | undefined;
};
};
export const DateRangeDropdown: React.FC<Props> = (props) => {
const {
applyButtonText = "Apply changes",
bothRequired = true,
buttonClassName,
buttonContainerClassName,
buttonFromDateClassName,
buttonToDateClassName,
buttonVariant,
cancelButtonText = "Cancel",
className,
disabled = false,
hideIcon = {
from: true,
to: true,
},
icon = <CalendarDays className="h-3 w-3 flex-shrink-0" />,
minDate,
maxDate,
onSelect,
placeholder = {
from: "Add date",
to: "Add date",
},
placement,
required = false,
showTooltip = false,
tabIndex,
value,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
const [dateRange, setDateRange] = useState<DateRange>(value);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
setDateRange({
from: value.from,
to: value.to,
});
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
toggleDropdown();
};
useOutsideClickDetector(dropdownRef, handleClose);
const disabledDays: Matcher[] = [];
if (minDate) disabledDays.push({ before: minDate });
if (maxDate) disabledDays.push({ after: maxDate });
useEffect(() => {
setDateRange(value);
}, [value]);
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (!isOpen) handleKeyDown(e);
} else handleKeyDown(e);
}}
>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Date range"
tooltipContent={
<>
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
{" - "}
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
</>
}
showTooltip={showTooltip}
variant={buttonVariant}
>
<span
className={cn(
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
buttonFromDateClassName
)}
>
{!hideIcon.from && icon}
{dateRange.from ? renderFormattedDate(dateRange.from) : placeholder.from}
</span>
<ArrowRight className="h-3 w-3 flex-shrink-0" />
<span
className={cn(
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
buttonToDateClassName
)}
>
{!hideIcon.to && icon}
{dateRange.to ? renderFormattedDate(dateRange.to) : placeholder.to}
</span>
</DropdownButton>
</button>
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 bg-custom-background-100 shadow-custom-shadow-rg rounded-md overflow-hidden p-3"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<DayPicker
selected={dateRange}
onSelect={(val) => {
// if both the dates are not required, immediately call onSelect
if (!bothRequired) onSelect(val);
setDateRange({
from: val?.from ?? undefined,
to: val?.to ?? undefined,
});
}}
mode="range"
disabled={disabledDays}
showOutsideDays
initialFocus
footer={
bothRequired && (
<div className="grid grid-cols-2 items-center gap-3.5 pt-6 relative">
<div className="absolute left-0 top-1 h-[0.5px] w-full border-t-[0.5px] border-custom-border-300" />
<Button
variant="neutral-primary"
onClick={() => {
setDateRange({
from: undefined,
to: undefined,
});
handleClose();
}}
>
{cancelButtonText}
</Button>
<Button
onClick={() => {
onSelect(dateRange);
handleClose();
}}
// if required, both the dates should be selected
// if not required, either both or none of the dates should be selected
disabled={required ? !(dateRange.from && dateRange.to) : !!dateRange.from !== !!dateRange.to}
>
{applyButtonText}
</Button>
</div>
)
}
/>
</div>
</Combobox.Options>
)}
</Combobox>
);
};

View File

@ -1,6 +1,6 @@
import React, { useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import DatePicker from "react-datepicker";
import { DayPicker, Matcher } from "react-day-picker";
import { usePopper } from "react-popper";
import { CalendarDays, X } from "lucide-react";
// hooks
@ -50,6 +50,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
tabIndex,
value,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
@ -102,18 +103,25 @@ export const DateDropdown: React.FC<Props> = (props) => {
useOutsideClickDetector(dropdownRef, handleClose);
const disabledDays: Matcher[] = [];
if (minDate) disabledDays.push({ before: minDate });
if (maxDate) disabledDays.push({ after: maxDate });
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
onKeyDown={handleKeyDown}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (!isOpen) handleKeyDown(e);
} else handleKeyDown(e);
}}
disabled={disabled}
>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
@ -123,6 +131,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
},
buttonContainerClassName
)}
ref={setReferenceElement}
onClick={handleOnClick}
>
<DropdownButton
@ -151,15 +160,22 @@ export const DateDropdown: React.FC<Props> = (props) => {
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<DatePicker
selected={value ? new Date(value) : null}
onChange={dropdownOnChange}
dateFormat="dd-MM-yyyy"
minDate={minDate}
maxDate={maxDate}
calendarClassName="shadow-custom-shadow-rg rounded"
inline
<div
className="my-1 bg-custom-background-100 shadow-custom-shadow-rg rounded-md overflow-hidden p-3"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => {
dropdownOnChange(date ?? null);
}}
showOutsideDays
initialFocus
disabled={disabledDays}
mode="single"
/>
</div>
</Combobox.Options>

View File

@ -1,5 +1,6 @@
export * from "./member";
export * from "./cycle";
export * from "./date-range";
export * from "./date";
export * from "./estimate";
export * from "./module";

View File

@ -182,7 +182,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate text-sm leading-5">
<span className="flex-grow truncate text-xs leading-5">
{Array.isArray(value) && value.length > 0
? value.length === 1
? getUserDetails(value[0])?.display_name

View File

@ -167,7 +167,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate text-sm leading-5">
<span className="flex-grow truncate text-xs leading-5">
{Array.isArray(value) && value.length > 0
? value.length === 1
? getUserDetails(value[0])?.display_name

View File

@ -1,7 +1,7 @@
import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import DatePicker from "react-datepicker";
import { DayPicker } from "react-day-picker";
import { Popover } from "@headlessui/react";
// hooks
import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store";
@ -266,15 +266,15 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
<Popover.Panel className="absolute right-0 z-10 mt-2 w-80 rounded-md bg-custom-background-100 p-2 shadow-lg">
{({ close }) => (
<div className="flex h-full w-full flex-col gap-y-1">
<DatePicker
selected={date ? new Date(date) : null}
onChange={(val) => {
if (!val) return;
setDate(val);
}}
dateFormat="dd-MM-yyyy"
minDate={tomorrow}
inline
<DayPicker
selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined}
onSelect={(date) => { if (!date) return; setDate(date) }}
mode="single"
className="border border-custom-border-200 rounded-md p-3"
disabled={[{
before: tomorrow,
}]}
/>
<Button
variant="primary"

View File

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { differenceInCalendarDays } from "date-fns";
import {
LinkIcon,
Signal,
@ -11,8 +12,7 @@ import {
XCircle,
CircleDot,
CopyPlus,
CalendarClock,
CalendarCheck2,
CalendarDays,
} from "lucide-react";
// hooks
import { useEstimate, useIssueDetail, useProject, useUser } from "hooks/store";
@ -36,10 +36,11 @@ import {
StateDropdown,
} from "components/dropdowns";
// icons
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { cn } from "helpers/common.helper";
// types
import type { TIssueOperations } from "./root";
@ -89,6 +90,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1;
return (
<>
{workspaceSlug && projectId && issue && (
@ -195,7 +198,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<CalendarClock className="h-4 w-4 flex-shrink-0" />
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<span>Start date</span>
</div>
<DateDropdown
@ -221,7 +224,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<span>Due date</span>
</div>
<DateDropdown
@ -237,9 +240,12 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
buttonClassName={cn("text-sm", {
"text-custom-text-400": !issue.target_date,
"text-red-500": targetDateDistance <= 0,
})}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100"
// TODO: add this logic
// showPlaceholderIcon
/>

View File

@ -1,7 +1,8 @@
import { useCallback, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
import { differenceInCalendarDays } from "date-fns";
import { Layers, Link, Paperclip } from "lucide-react";
import xor from "lodash/xor";
// hooks
import { useEventTracker, useEstimate, useLabel, useIssues } from "hooks/store";
@ -230,6 +231,8 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1;
return (
<div className={className}>
{/* basic properties */}
@ -279,7 +282,6 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<DateDropdown
value={issue.start_date ?? null}
onChange={handleStartDate}
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
maxDate={maxDate ?? undefined}
placeholder="Start date"
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
@ -295,10 +297,11 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<DateDropdown
value={issue?.target_date ?? null}
onChange={handleTargetDate}
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
minDate={minDate ?? undefined}
placeholder="Due date"
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
buttonClassName={targetDateDistance <= 0 ? "text-red-500" : ""}
clearIconClassName="!text-custom-text-100"
disabled={isReadOnly}
showTooltip
/>
@ -375,7 +378,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="sub_issue_count"
shouldRenderProperty={!!issue?.sub_issues_count}
shouldRenderProperty={(properties) => !!properties.sub_issue_count}
>
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
<div
@ -392,7 +395,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="attachment_count"
shouldRenderProperty={!!issue?.attachment_count}
shouldRenderProperty={(properties) => !!properties.attachment_count}
>
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
@ -406,7 +409,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="link"
shouldRenderProperty={!!issue?.link_count}
shouldRenderProperty={(properties) => !!properties.link}
>
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">

View File

@ -4,16 +4,20 @@ import { IIssueDisplayProperties } from "@plane/types";
interface IWithDisplayPropertiesHOC {
displayProperties: IIssueDisplayProperties;
shouldRenderProperty?: boolean;
displayPropertyKey: keyof IIssueDisplayProperties;
shouldRenderProperty?: (displayProperties: IIssueDisplayProperties) => boolean;
displayPropertyKey: keyof IIssueDisplayProperties | (keyof IIssueDisplayProperties)[];
children: ReactNode;
}
export const WithDisplayPropertiesHOC = observer(
({ displayProperties, shouldRenderProperty = true, displayPropertyKey, children }: IWithDisplayPropertiesHOC) => {
const shouldDisplayPropertyFromFilters = displayProperties[displayPropertyKey];
({ displayProperties, shouldRenderProperty, displayPropertyKey, children }: IWithDisplayPropertiesHOC) => {
let shouldDisplayPropertyFromFilters = false;
if (Array.isArray(displayPropertyKey))
shouldDisplayPropertyFromFilters = displayPropertyKey.every((key) => !!displayProperties[key]);
else shouldDisplayPropertyFromFilters = !!displayProperties[displayPropertyKey];
const renderProperty = shouldDisplayPropertyFromFilters && shouldRenderProperty;
const renderProperty =
shouldDisplayPropertyFromFilters && (shouldRenderProperty ? shouldRenderProperty(displayProperties) : true);
if (!renderProperty) return null;

View File

@ -1,11 +1,13 @@
import React from "react";
import { observer } from "mobx-react-lite";
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
// components
import { DateDropdown } from "components/dropdowns";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
import { cn } from "helpers/common.helper";
type Props = {
issue: TIssue;
@ -17,6 +19,8 @@ type Props = {
export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props) => {
const { issue, onChange, disabled, onClose } = props;
const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1;
return (
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
@ -36,8 +40,11 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
disabled={disabled}
placeholder="Due date"
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
buttonClassName={cn("rounded-none text-left", {
"text-red-500": targetDateDistance <= 0,
})}
clearIconClassName="!text-custom-text-100"
onClose={onClose}
/>
</div>

View File

@ -34,7 +34,7 @@ export const IssueColumn = observer((props: Props) => {
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey={property}
shouldRenderProperty={shouldRenderProperty}
shouldRenderProperty={() => shouldRenderProperty}
>
<td
tabIndex={0}

View File

@ -25,7 +25,7 @@ export const SpreadsheetHeaderColumn = observer((props: Props) => {
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey={property}
shouldRenderProperty={shouldRenderProperty}
shouldRenderProperty={() => shouldRenderProperty}
>
<th
className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100"

View File

@ -494,14 +494,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => {
onChange(date ? renderFormattedPayloadDate(date) : null);
handleFormChange();
}}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Start date"
maxDate={maxDate ?? undefined}
tabIndex={10}
placeholder="Start date"
/>
</div>
)}
@ -513,14 +509,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => {
onChange(date ? renderFormattedPayloadDate(date) : null);
handleFormChange();
}}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Due date"
minDate={minDate ?? undefined}
tabIndex={11}
placeholder="Due date"
/>
</div>
)}
@ -539,7 +531,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
}}
value={value}
buttonVariant="border-with-text"
tabIndex={12}
tabIndex={11}
/>
</div>
)}
@ -559,7 +551,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
handleFormChange();
}}
buttonVariant="border-with-text"
tabIndex={13}
tabIndex={12}
multiple
showCount
/>
@ -581,7 +573,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
}}
projectId={projectId}
buttonVariant="border-with-text"
tabIndex={14}
tabIndex={13}
/>
</div>
)}
@ -611,7 +603,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
</button>
}
placement="bottom-start"
tabIndex={15}
tabIndex={14}
>
{watch("parent_id") ? (
<>
@ -661,7 +653,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
onKeyDown={(e) => {
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
}}
tabIndex={16}
tabIndex={15}
>
<div className="flex cursor-pointer items-center justify-center">
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
@ -669,7 +661,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<span className="text-xs">Create more</span>
</div>
<div className="flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={17}>
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={16}>
Discard
</Button>
@ -681,7 +673,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
size="sm"
loading={isSubmitting}
onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))}
tabIndex={18}
tabIndex={17}
>
{isSubmitting ? "Moving" : "Move from draft"}
</Button>
@ -691,7 +683,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
size="sm"
loading={isSubmitting}
onClick={handleSubmit((data) => handleFormSubmit(data, true))}
tabIndex={18}
tabIndex={17}
>
{isSubmitting ? "Saving" : "Save as draft"}
</Button>
@ -699,7 +691,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
</Fragment>
)}
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={isDraft ? 19 : 18}>
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={isDraft ? 18 : 17}>
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"}
</Button>
</div>

View File

@ -1,16 +1,7 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import {
Signal,
Tag,
Triangle,
LayoutPanelTop,
CircleDot,
CopyPlus,
XCircle,
CalendarClock,
CalendarCheck2,
} from "lucide-react";
import { differenceInCalendarDays } from "date-fns";
import { Signal, Tag, Triangle, LayoutPanelTop, CircleDot, CopyPlus, XCircle, CalendarDays } from "lucide-react";
// hooks
import { useIssueDetail, useProject } from "hooks/store";
// ui icons
@ -33,6 +24,8 @@ import {
} from "components/dropdowns";
// components
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// helpers
import { cn } from "helpers/common.helper";
interface IPeekOverviewProperties {
workspaceSlug: string;
@ -61,6 +54,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1;
return (
<div className="mt-1">
<h6 className="text-sm font-medium">Properties</h6>
@ -129,7 +124,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* start date */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CalendarClock className="h-4 w-4 flex-shrink-0" />
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<span>Start date</span>
</div>
<DateDropdown
@ -156,7 +151,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* due date */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<span>Due date</span>
</div>
<DateDropdown
@ -172,9 +167,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
disabled={disabled}
className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
buttonClassName={cn("text-sm", {
"text-custom-text-400": !issue.target_date,
"text-red-500": targetDateDistance <= 0,
})}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100"
// TODO: add this logic
// showPlaceholderIcon
/>

View File

@ -2,7 +2,7 @@ import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// components
import { ModuleStatusSelect } from "components/modules";
import { DateDropdown, ProjectDropdown, ProjectMemberDropdown } from "components/dropdowns";
import { DateRangeDropdown, ProjectDropdown, ProjectMemberDropdown } from "components/dropdowns";
// ui
import { Button, Input, TextArea } from "@plane/ui";
// helpers
@ -27,18 +27,12 @@ const defaultValues: Partial<IModule> = {
member_ids: [],
};
export const ModuleForm: React.FC<Props> = ({
handleFormSubmit,
handleClose,
status,
projectId,
setActiveProject,
data,
}) => {
export const ModuleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
// form info
const {
formState: { errors, isSubmitting, dirtyFields },
handleSubmit,
watch,
control,
reset,
} = useForm<IModule>({
@ -67,15 +61,6 @@ export const ModuleForm: React.FC<Props> = ({
});
}, [data, reset]);
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
return (
<form onSubmit={handleSubmit(handleCreateUpdateModule)}>
<div className="space-y-5">
@ -152,36 +137,39 @@ export const ModuleForm: React.FC<Props> = ({
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Start date"
maxDate={maxDate ?? undefined}
tabIndex={3}
/>
</div>
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
<Controller
control={control}
name="target_date"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown
buttonVariant="border-with-text"
className="h-7"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
}}
placeholder={{
from: "Start date",
to: "End date",
}}
hideIcon={{
to: true,
}}
tabIndex={3}
/>
)}
/>
)}
/>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Target date"
minDate={minDate ?? undefined}
tabIndex={4}
/>
</div>
)}
/>
<ModuleStatusSelect control={control} error={errors.status} tabIndex={5} />
<div className="h-7">
<ModuleStatusSelect control={control} error={errors.status} tabIndex={4} />
</div>
<Controller
control={control}
name="lead_id"
@ -194,7 +182,7 @@ export const ModuleForm: React.FC<Props> = ({
multiple={false}
buttonVariant="border-with-text"
placeholder="Lead"
tabIndex={6}
tabIndex={5}
/>
</div>
)}
@ -212,7 +200,7 @@ export const ModuleForm: React.FC<Props> = ({
buttonVariant={value && value.length > 0 ? "transparent-without-text" : "border-with-text"}
buttonClassName={value && value.length > 0 ? "hover:bg-transparent px-0" : ""}
placeholder="Members"
tabIndex={7}
tabIndex={6}
/>
</div>
)}
@ -221,10 +209,10 @@ export const ModuleForm: React.FC<Props> = ({
</div>
</div>
<div className="mt-5 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200 pt-5">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={8}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={7}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={9}>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={8}>
{status ? (isSubmitting ? "Updating" : "Update module") : isSubmitting ? "Creating" : "Create module"}
</Button>
</div>

View File

@ -1,11 +1,10 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
import { Disclosure, Transition } from "@headlessui/react";
import {
AlertCircle,
CalendarCheck2,
CalendarClock,
ChevronDown,
ChevronRight,
@ -22,12 +21,11 @@ import useToast from "hooks/use-toast";
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
import { DeleteModuleModal } from "components/modules";
import ProgressChart from "components/core/sidebar/progress-chart";
import { ProjectMemberDropdown } from "components/dropdowns";
import { DateRangeDropdown, ProjectMemberDropdown } from "components/dropdowns";
// ui
import { CustomRangeDatePicker } from "components/ui";
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
// helpers
import { isDateGreaterThanToday, renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
@ -56,9 +54,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
// refs
const startDateButtonRef = useRef<HTMLButtonElement | null>(null);
const endDateButtonRef = useRef<HTMLButtonElement | null>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId, peekModule } = router.query;
@ -72,7 +67,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast();
const { setValue, watch, reset, control } = useForm({
const { reset, control } = useForm({
defaultValues,
});
@ -85,7 +80,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" },
});
})
.catch((_) => {
.catch(() => {
captureModuleEvent({
eventName: MODULE_UPDATED,
payload: { ...data, state: "FAILED" },
@ -187,40 +182,16 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
});
};
const handleStartDateChange = async (date: string) => {
setValue("start_date", date);
if (!watch("target_date") || watch("target_date") === "") endDateButtonRef.current?.click();
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
submitChanges({
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
target_date: renderFormattedPayloadDate(`${watch("target_date")}`),
});
setToastAlert({
type: "success",
title: "Success!",
message: "Module updated successfully.",
});
}
};
const handleEndDateChange = async (date: string) => {
setValue("target_date", date);
if (!watch("start_date") || watch("start_date") === "") endDateButtonRef.current?.click();
if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") {
submitChanges({
target_date: renderFormattedPayloadDate(`${watch("target_date")}`),
start_date: renderFormattedPayloadDate(`${watch("start_date")}`),
});
setToastAlert({
type: "success",
title: "Success!",
message: "Module updated successfully.",
});
}
const handleDateChange = async (startDate: Date | undefined, targetDate: Date | undefined) => {
submitChanges({
start_date: startDate ? renderFormattedPayloadDate(startDate) : null,
target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null,
});
setToastAlert({
type: "success",
title: "Success!",
message: "Module updated successfully.",
});
};
useEffect(() => {
@ -257,9 +228,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
</Loader>
);
const startDate = new Date(watch("start_date") ?? moduleDetails.start_date ?? "");
const endDate = new Date(watch("target_date") ?? moduleDetails.target_date ?? "");
const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status);
const issueCount =
@ -319,7 +287,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<Controller
control={control}
name="status"
render={({ field: { value, onChange } }) => (
render={({ field: { value } }) => (
<CustomSelect
customButton={
<span
@ -361,118 +329,42 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
</span>
)}
<div className="flex flex-col gap-5 pb-6 pt-2.5">
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<CalendarClock className="h-4 w-4" />
<span className="text-base">Start date</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={startDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("start_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(startDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : moduleDetails?.start_date}
onChange={(val) => {
if (val) {
handleStartDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("target_date")}`)}
selectsStart={watch("target_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<CalendarClock className="h-4 w-4" />
<span className="text-base">Date range</span>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<CalendarCheck2 className="h-4 w-4" />
<span className="text-base">Target date</span>
</div>
<div className="relative flex w-1/2 items-center rounded-sm">
<Popover className="flex h-full w-full items-center justify-center rounded-lg">
{({ close }) => (
<>
<Popover.Button
ref={endDateButtonRef}
className={`w-full cursor-pointer rounded-sm text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
<span
className={`group flex w-full items-center justify-between gap-2 px-1.5 py-1 text-sm ${
watch("target_date") ? "" : "text-custom-text-400"
}`}
>
{renderFormattedDate(endDate) ?? "No date selected"}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 top-10 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("target_date") ? watch("target_date") : moduleDetails?.target_date}
onChange={(val) => {
if (val) {
handleEndDateChange(val);
close();
}
}}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<div className="w-3/5 h-7">
<Controller
control={control}
name="start_date"
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
<Controller
control={control}
name="target_date"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown
buttonContainerClassName="w-full"
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}}
placeholder={{
from: "Start date",
to: "Target date",
}}
/>
)}
/>
)}
/>
</div>
</div>
@ -484,7 +376,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex flex-col gap-5 pb-6 pt-2.5">
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<UserCircle2 className="h-4 w-4" />
<span className="text-base">Lead</span>
</div>
@ -492,7 +384,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
control={control}
name="lead_id"
render={({ field: { value } }) => (
<div className="w-1/2">
<div className="w-3/5 h-7">
<ProjectMemberDropdown
value={value ?? null}
onChange={(val) => {
@ -508,7 +400,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
/>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<UserGroupIcon className="h-4 w-4" />
<span className="text-base">Members</span>
</div>
@ -516,7 +408,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
control={control}
name="member_ids"
render={({ field: { value } }) => (
<div className="w-1/2">
<div className="w-3/5 h-7">
<ProjectMemberDropdown
value={value ?? []}
onChange={(val: string[]) => {
@ -532,13 +424,12 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
)}
/>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<LayersIcon className="h-4 w-4" />
<span className="text-base">Issues</span>
</div>
<div className="flex w-1/2 items-center">
<div className="h-7 w-3/5 flex items-center">
<span className="px-1.5 text-sm text-custom-text-300">{issueCount}</span>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { Fragment, FC } from "react";
import { useRouter } from "next/router";
import { useForm, Controller } from "react-hook-form";
import { DateDropdown } from "components/dropdowns";
import { Transition, Dialog } from "@headlessui/react";
import { X } from "lucide-react";
// constants
@ -9,7 +10,6 @@ import { allTimeIn30MinutesInterval12HoursFormat } from "constants/notification"
import useToast from "hooks/use-toast";
// ui
import { Button, CustomSelect } from "@plane/ui";
import { CustomDatePicker } from "components/ui";
// types
import type { IUserNotification } from "@plane/types";
@ -169,17 +169,18 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
control={control}
rules={{ required: "Please select a date" }}
render={({ field: { value, onChange } }) => (
<CustomDatePicker
placeholder="Select date"
<DateDropdown
value={value}
placeholder="Select date"
onChange={(val) => {
setValue("time", null);
onChange(val);
}}
className="w-full rounded-md border border-custom-border-300 bg-custom-background-100 px-3 py-2 !text-sm text-custom-text-100 placeholder:!text-custom-text-400 focus:outline-none"
wrapperClassName="w-full"
noBorder
minDate={new Date()}
buttonVariant="border-with-text"
buttonContainerClassName="w-full text-left"
buttonClassName="border-custom-border-300 px-3 py-2.5"
hideIcon
/>
)}
/>
@ -213,11 +214,10 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
onClick={() => {
setValue("period", "AM");
}}
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
watch("period") === "AM"
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "AM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
}`}
>
AM
</div>
@ -225,11 +225,10 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
onClick={() => {
setValue("period", "PM");
}}
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
watch("period") === "PM"
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "PM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
}`}
>
PM
</div>

View File

@ -1,69 +0,0 @@
// react-datepicker
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
type Props = {
renderAs?: "input" | "button";
value: Date | string | null | undefined;
onChange: (val: string | null) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
customInput?: React.ReactNode;
placeholder?: string;
displayShortForm?: boolean;
error?: boolean;
noBorder?: boolean;
wrapperClassName?: string;
className?: string;
isClearable?: boolean;
disabled?: boolean;
maxDate?: Date;
minDate?: Date;
};
export const CustomDatePicker: React.FC<Props> = ({
renderAs = "button",
value,
onChange,
handleOnOpen,
handleOnClose,
placeholder = "Select date",
error = false,
noBorder = false,
wrapperClassName = "",
className = "",
isClearable = true,
disabled = false,
customInput,
maxDate,
minDate,
}) => (
<DatePicker
placeholderText={placeholder}
selected={value ? new Date(value) : null}
onChange={(val) => {
if (!val) onChange(null);
else onChange(renderFormattedPayloadDate(val));
}}
onCalendarOpen={handleOnOpen}
onCalendarClose={handleOnClose}
wrapperClassName={wrapperClassName}
customInput={customInput}
className={`${
renderAs === "input"
? "block px-2 py-2 text-sm focus:outline-none"
: renderAs === "button"
? `px-2 py-1 text-xs shadow-sm ${disabled ? "" : "hover:bg-custom-background-80"} duration-300`
: ""
} ${error ? "border-red-500 bg-red-100" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"} ${
noBorder ? "" : "border border-custom-border-200"
} w-full rounded-md caret-transparent outline-none ${className}`}
dateFormat="MMM dd, yyyy"
isClearable={Boolean(isClearable && !disabled)}
disabled={disabled}
maxDate={maxDate}
minDate={minDate}
/>
);

View File

@ -1,10 +1,8 @@
export * from "./graphs";
export * from "./datepicker";
export * from "./empty-space";
export * from "./labels-list";
export * from "./multi-level-dropdown";
export * from "./markdown-to-component";
export * from "./integration-and-import-export-banner";
export * from "./range-datepicker";
export * from "./profile-empty-state";
export * from "./loader";

View File

@ -1,65 +0,0 @@
// react-datepicker
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
type Props = {
renderAs?: "input" | "button";
value: Date | string | null | undefined;
onChange: (val: string | null) => void;
error?: boolean;
className?: string;
isClearable?: boolean;
disabled?: boolean;
startDate: string | null;
endDate: string | null;
selectsStart?: boolean;
selectsEnd?: boolean;
minDate?: Date | null | undefined;
maxDate?: Date | null | undefined;
};
export const CustomRangeDatePicker: React.FC<Props> = ({
renderAs = "button",
value,
onChange,
error = false,
className = "",
disabled = false,
startDate,
endDate,
selectsStart = false,
selectsEnd = false,
minDate = null,
maxDate = null,
}) => (
<DatePicker
selected={value ? new Date(value) : null}
onChange={(val) => {
if (!val) onChange(null);
else onChange(renderFormattedPayloadDate(val));
}}
className={`${
renderAs === "input"
? "block px-3 py-2 text-sm focus:outline-none"
: renderAs === "button"
? `px-3 py-1 text-xs shadow-sm ${
disabled ? "" : "hover:bg-custom-background-80"
} duration-300 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary`
: ""
} ${error ? "border-red-500 bg-red-100" : ""} ${
disabled ? "cursor-not-allowed" : "cursor-pointer"
} w-full rounded-md border border-custom-border-200 bg-transparent caret-transparent ${className}`}
dateFormat="dd-MM-yyyy"
disabled={disabled}
selectsStart={selectsStart}
selectsEnd={selectsEnd}
startDate={startDate ? new Date(startDate) : new Date()}
endDate={endDate ? new Date(endDate) : new Date()}
minDate={minDate}
maxDate={maxDate}
shouldCloseOnSelect
inline
/>
);

View File

@ -106,14 +106,14 @@ export const ISSUE_DISPLAY_PROPERTIES: {
title: string;
}[] = [
{ key: "assignee", title: "Assignee" },
{ key: "start_date", title: "Start Date" },
{ key: "due_date", title: "Due Date" },
{ key: "start_date", title: "Start date" },
{ key: "due_date", title: "Due date" },
{ key: "key", title: "ID" },
{ key: "labels", title: "Labels" },
{ key: "priority", title: "Priority" },
{ key: "state", title: "State" },
{ key: "sub_issue_count", title: "Sub Issue Count" },
{ key: "attachment_count", title: "Attachment Count" },
{ key: "sub_issue_count", title: "Sub issue count" },
{ key: "attachment_count", title: "Attachment count" },
{ key: "link", title: "Link" },
{ key: "estimate", title: "Estimate" },
{ key: "modules", title: "Modules" },

View File

@ -1,6 +1,6 @@
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui";
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip } from "lucide-react";
import { FC } from "react";
import { ISvgIcons } from "@plane/ui/src/icons/type";
import {
@ -60,7 +60,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
ascendingOrderTitle: "New",
descendingOrderKey: "target_date",
descendingOrderTitle: "Old",
icon: CalendarCheck,
icon: CalendarDays,
Column: SpreadsheetDueDateColumn,
},
estimate: {
@ -114,7 +114,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
ascendingOrderTitle: "New",
descendingOrderKey: "start_date",
descendingOrderTitle: "Old",
icon: CalendarClock,
icon: CalendarDays,
Column: SpreadsheetStartDateColumn,
},
state: {

View File

@ -1,4 +1,5 @@
import { differenceInDays, format, formatDistanceToNow, isAfter, isValid, parseISO } from "date-fns";
import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns";
import { isNil } from "lodash";
// Format Date Helpers
/**
@ -172,3 +173,23 @@ export const getWeekNumberOfDate = (date: Date): number => {
const weekNumber = Math.ceil((days + 1) / 7);
return weekNumber;
};
/**
* @returns {boolean} boolean value depending on whether the dates are equal
* @description Returns boolean value depending on whether the dates are equal
* @param date1
* @param date2
* @example checkIfDatesAreEqual("2024-01-01", "2024-01-01") // true
* @example checkIfDatesAreEqual("2024-01-01", "2024-01-02") // false
*/
export const checkIfDatesAreEqual = (
date1: Date | string | null | undefined,
date2: Date | string | null | undefined
): boolean => {
if (isNil(date1) && isNil(date2)) return true;
if (isNil(date1) || isNil(date2)) return false;
const parsedDate1 = new Date(date1);
const parsedDate2 = new Date(date2);
return isEqual(parsedDate1, parsedDate2);
};

View File

@ -48,7 +48,7 @@
"posthog-js": "^1.105.0",
"react": "18.2.0",
"react-color": "^2.19.3",
"react-datepicker": "^4.8.0",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.38.0",
@ -67,7 +67,6 @@
"@types/nprogress": "^0.2.0",
"@types/react": "^18.2.42",
"@types/react-color": "^3.0.6",
"@types/react-datepicker": "^4.8.0",
"@types/react-dom": "^18.2.17",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.48.2",

View File

@ -5,7 +5,7 @@ import { AppProps } from "next/app";
import "styles/globals.css";
import "styles/command-pallette.css";
import "styles/nprogress.css";
import "styles/react-datepicker.css";
import "styles/react-day-picker.css";
// constants
import { SITE_TITLE } from "constants/seo-variables";
// mobx store provider

View File

@ -1,142 +0,0 @@
.react-datepicker-wrapper input::placeholder {
color: rgba(var(--color-text-200));
opacity: 1;
}
.react-datepicker-wrapper input:-ms-input-placeholder {
color: rgba(var(--color-text-200));
}
.react-datepicker-wrapper .react-datepicker__close-icon::after {
background: transparent;
color: rgba(var(--color-text-200));
}
.react-datepicker-popper {
z-index: 30 !important;
}
.react-datepicker-wrapper {
position: relative;
background-color: rgba(var(--color-background-100)) !important;
}
.react-datepicker-wrapper,
.react-datepicker__input-container {
border-radius: 0.375rem;
}
.react-datepicker {
font-family: "Inter" !important;
background-color: rgba(var(--color-background-100)) !important;
border: 0.5px solid rgba(var(--color-border-300)) !important;
}
.react-datepicker__month-container {
width: 300px;
background-color: rgba(var(--color-background-100)) !important;
color: rgba(var(--color-text-100)) !important;
border-radius: 10px !important;
/* border: 1px solid rgba(var(--color-background-80)) !important; */
}
.react-datepicker__header {
border-radius: 10px !important;
background-color: rgba(var(--color-background-100)) !important;
border: none !important;
}
.react-datepicker__navigation {
line-height: 0.78;
}
.react-datepicker__triangle {
border-color: rgba(var(--color-background-100)) transparent transparent transparent !important;
}
.react-datepicker__triangle:before {
border-bottom-color: rgba(var(--color-background-80)) !important;
}
.react-datepicker__triangle:after {
border-bottom-color: rgba(var(--color-background-100)) !important;
}
.react-datepicker__current-month {
font-weight: 500 !important;
color: rgba(var(--color-text-100)) !important;
}
.react-datepicker__month {
border-collapse: collapse;
color: rgba(var(--color-text-100)) !important;
}
.react-datepicker__day-names {
margin-top: 10px;
margin-left: 14px;
width: 280px;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0;
}
.react-datepicker__day-name {
color: rgba(var(--color-text-200)) !important;
}
.react-datepicker__week {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-left: 8px;
}
.react-datepicker__day {
color: rgba(var(--color-text-100)) !important;
}
.react-datepicker__day {
border-radius: 50% !important;
transition: all 0.15s ease-in-out;
}
.react-datepicker__day:hover {
background-color: rgba(var(--color-background-80)) !important;
color: rgba(var(--color-text-100)) !important;
}
.react-datepicker__day--selected,
.react-datepicker__day--selected:hover {
background-color: #216ba5 !important;
color: white !important;
}
.react-datepicker__day--disabled,
.react-datepicker__day--disabled:hover {
background: transparent !important;
color: rgba(var(--color-text-400)) !important;
cursor: default;
}
.react-datepicker__day--today {
font-weight: 800;
}
.react-datepicker__day--highlighted {
background-color: rgba(var(--color-background-80)) !important;
}
.react-datepicker__day--keyboard-selected {
background-color: #216ba5 !important;
color: white !important;
}
.react-datepicker__day--in-range-start,
.react-datepicker__day--in-range-end {
background-color: rgba(var(--color-primary-100)) !important;
color: white !important;
}
.react-datepicker__day--in-range:not(.react-datepicker__day--in-range-start):not(.react-datepicker__day--in-range-end) {
background-color: rgba(var(--color-primary-100)) !important;
color: white !important;
}

View File

@ -0,0 +1,368 @@
.rdp {
font-size: 12px;
--rdp-cell-size: 40px;
/* Size of the day cells. */
--rdp-caption-font-size: 1.15rem;
/* Font size for the caption labels. */
--rdp-caption-navigation-size: 1.25rem;
/* Font size for the caption labels. */
--rdp-accent-color: rgba(var(--color-primary-100));
/* Accent color for the background of selected days. */
--rdp-background-color: rgba(var(--color-primary-100), 0.2);
/* Background color for the hovered/focused elements. */
--rdp-dark-background-color: rgba(var(--color-primary-300));
/* Background color for the hovered/focused, already selected elements. */
--rdp-outline: 2px solid var(--rdp-accent-color);
/* Outline border for focused elements */
--rdp-selected-color: #ffffff;
/* Color of selected day text */
background: transparent;
}
/* Hide elements for devices that are not screen readers */
.rdp-vhidden {
box-sizing: border-box;
padding: 0;
margin: 0;
background: transparent;
border: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
position: absolute !important;
top: 0;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
overflow: hidden !important;
clip: rect(1px, 1px, 1px, 1px) !important;
border: 0 !important;
}
/* Buttons */
.rdp-button_reset {
appearance: none;
position: relative;
margin: 0;
padding: 0;
cursor: default;
color: inherit;
background: none;
font: inherit;
-moz-appearance: none;
-webkit-appearance: none;
}
.rdp-button_reset:focus-visible {
/* Make sure to reset outline only when :focus-visible is supported */
outline: none;
}
.rdp-button {
border: 2px solid transparent;
}
.rdp-button[disabled]:not(.rdp-day_selected) {
opacity: 0.25;
}
.rdp-button:not([disabled]) {
cursor: pointer;
}
.rdp-button:focus-visible:not([disabled]):not(.rdp-day_selected) {
color: inherit;
background-color: var(--rdp-background-color);
}
.rdp-button:focus-visible:not([disabled]).rdp-day_selected:not(.rdp-day_range_middle) {
outline: var(--rdp-outline);
outline-offset: 2px;
background-color: var(--rdp-dark-background-color);
outline-width: thin;
}
.rdp-button:hover:not([disabled]).rdp-day_selected {
background-color: var(--rdp-dark-background-color);
}
.rdp-button:hover:not([disabled]):not(.rdp-day_selected) {
background-color: var(--rdp-background-color);
}
.rdp-months {
display: flex;
}
.rdp-month {
margin: 0 1em;
}
.rdp-month:first-child {
margin-left: 0;
}
.rdp-month:last-child {
margin-right: 0;
}
.rdp-table {
margin: 0;
max-width: calc(var(--rdp-cell-size) * 7);
border-collapse: collapse;
}
.rdp-with_weeknumber .rdp-table {
max-width: calc(var(--rdp-cell-size) * 8);
border-collapse: collapse;
}
.rdp-caption {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
text-align: left;
}
.rdp-multiple_months .rdp-caption {
position: relative;
display: block;
text-align: center;
}
.rdp-caption_dropdowns {
position: relative;
display: inline-flex;
}
.rdp-caption_label {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
margin: 0;
padding: 0 0.25em;
white-space: nowrap;
color: currentColor;
border: 0;
border: 2px solid transparent;
font-family: inherit;
font-size: var(--rdp-caption-font-size);
font-weight: 600;
}
.rdp-nav {
white-space: nowrap;
}
.rdp-multiple_months .rdp-caption_start .rdp-nav {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.rdp-multiple_months .rdp-caption_end .rdp-nav {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
}
.rdp-nav_button {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--rdp-caption-navigation-size);
height: var(--rdp-caption-navigation-size);
padding: 0.25em;
border-radius: 2px;
}
.rdp-nav_button:hover,
.rdp-nav_button:focus-visible {
background-color: rgba(var(--color-background-80)) !important;
}
/* ---------- */
/* Dropdowns */
/* ---------- */
.rdp-dropdown_year,
.rdp-dropdown_month {
position: relative;
display: inline-flex;
align-items: center;
}
.rdp-dropdown {
appearance: none;
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
cursor: inherit;
opacity: 0;
border: none;
background-color: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.rdp-dropdown[disabled] {
opacity: unset;
color: unset;
}
.rdp-dropdown:focus-visible:not([disabled])+.rdp-caption_label {
background-color: var(--rdp-background-color);
border: var(--rdp-outline);
border-radius: 6px;
}
.rdp-dropdown_icon {
margin: 0 0 0 5px;
}
.rdp-head {
border: 0;
}
.rdp-head_row,
.rdp-row {
height: 100%;
}
.rdp-head_cell {
vertical-align: middle;
font-size: 0.75em;
font-weight: 700;
text-align: center;
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-transform: uppercase;
}
.rdp-tbody {
border: 0;
}
.rdp-tfoot {
margin: 0.5em;
}
.rdp-cell {
width: var(--rdp-cell-size);
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-align: center;
}
.rdp-weeknumber {
font-size: 0.75em;
}
.rdp-weeknumber,
.rdp-day {
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--rdp-cell-size);
max-width: var(--rdp-cell-size);
height: var(--rdp-cell-size);
margin: 0;
border: 2px solid transparent;
border-radius: 100%;
}
.rdp-day_today:not(.rdp-day_outside) {
position: relative;
}
.rdp-day_today:not(.rdp-day_outside)::after {
content: '';
position: absolute;
left: 50%;
bottom: 2px;
width: 0.5em;
height: 0.5em;
background-color: var(--rdp-background-color);
border-radius: 100%;
transform: translate(-50%, 0);
}
.rdp-day_selected,
.rdp-day_selected:focus-visible,
.rdp-day_selected:hover {
color: var(--rdp-selected-color);
opacity: 1;
background-color: var(--rdp-accent-color);
}
.rdp-day_outside:not(.rdp-day_selected) {
opacity: 0.5;
}
.rdp-day_selected:focus-visible {
z-index: 1;
}
td:has(.rdp-day_range_start),
td:has(.rdp-day_range_middle),
td:has(.rdp-day_range_end) {
position: relative;
}
td:has(.rdp-day_range_start)::before,
td:has(.rdp-day_range_middle)::before,
td:has(.rdp-day_range_end)::before {
content: '';
position: absolute;
background-color: var(--rdp-background-color);
top: 50%;
height: 100%;
width: 50%;
transform: translate(0, -50%);
}
td:has(.rdp-day_range_start)::before {
left: 50%;
}
td:has(.rdp-day_range_middle)::before {
left: 50%;
width: 100%;
transform: translate(-50%, -50%);
}
td:has(.rdp-day_range_end)::before {
right: 50%;
}
td:has(.rdp-day_range_start.rdp-day_range_end)::before {
display: none;
}
.rdp-day_range_middle {
background-color: transparent;
color: inherit;
}
.rdp-day_range_middle:hover,
.rdp-day_range_middle:focus-visible {
background-color: var(--rdp-background-color) !important;
color: inherit !important;
}

View File

@ -1865,7 +1865,7 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@popperjs/core@^2.11.7", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.0", "@popperjs/core@^2.9.2":
"@popperjs/core@^2.11.7", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.0":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
@ -2791,16 +2791,6 @@
"@types/react" "*"
"@types/reactcss" "*"
"@types/react-datepicker@^4.8.0":
version "4.19.4"
resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.19.4.tgz#07807187ead4c7025bc0039fe05eb5713298f3e4"
integrity sha512-HRD0LHTxBVe61LRJgTdPscbapLQl7+jI/7bxnPGpvzdJ/iXN9q7ucYv8HKULeIAN84O5LzFhwTMOkO4QnIUJaQ==
dependencies:
"@popperjs/core" "^2.9.2"
"@types/react" "*"
date-fns "^2.0.1"
react-popper "^2.2.5"
"@types/react-dom@^18.2.17":
version "18.2.18"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
@ -3617,7 +3607,7 @@ class-variance-authority@^0.7.0:
dependencies:
clsx "2.0.0"
classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2:
classnames@^2.3.1, classnames@^2.3.2:
version "2.5.0"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.0.tgz#9304d63d8d7135989e33e1ffb0bcc65178f92c2a"
integrity sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==
@ -3918,7 +3908,7 @@ dash-get@^1.0.2:
resolved "https://registry.yarnpkg.com/dash-get/-/dash-get-1.0.2.tgz#4c9e9ad5ef04c4bf9d3c9a451f6f7997298dcc7c"
integrity sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==
date-fns@^2.0.1, date-fns@^2.30.0:
date-fns@^2.30.0:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
@ -7237,7 +7227,7 @@ progress@^2.0.0, progress@^2.0.3:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -7481,17 +7471,10 @@ react-css-styled@^1.1.9:
css-styled "~1.0.8"
framework-utils "^1.1.0"
react-datepicker@^4.8.0:
version "4.25.0"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.25.0.tgz#86b3ee8ac764bad1650046d0cf9280837bf6d845"
integrity sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==
dependencies:
"@popperjs/core" "^2.11.8"
classnames "^2.2.6"
date-fns "^2.30.0"
prop-types "^15.7.2"
react-onclickoutside "^6.13.0"
react-popper "^2.3.0"
react-day-picker@^8.10.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.0.tgz#729c5b9564967a924213978fb9c0751884a60595"
integrity sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==
react-dom@18.2.0, react-dom@^18.2.0:
version "18.2.0"
@ -7575,11 +7558,6 @@ react-moveable@^0.54.2:
react-css-styled "^1.1.9"
react-selecto "^1.25.0"
react-onclickoutside@^6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc"
integrity sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==
react-popper@^1.3.11:
version "1.3.11"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.11.tgz#a2cc3f0a67b75b66cfa62d2c409f9dd1fcc71ffd"
@ -7593,7 +7571,7 @@ react-popper@^1.3.11:
typed-styles "^0.0.7"
warning "^4.0.2"
react-popper@^2.2.5, react-popper@^2.3.0:
react-popper@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==