forked from github/plane
fix: bug and ui fix (#1073)
* fix: cycle and module sidebar scroll * style: date picker theming * style: workspace slug spacing * fix: app sidebar z-index * fix: favorite cycle mutation * fix: cycle modal on error close * feat: cycle view context * style: active cycle stats scroll * fix: active cycle favorite mutation * feat: import export banner * feat: cycle sidebar date picker logic updated * fix: NaN in progress percentage fix * fix: tooltip fix * style: empty state for active cycle * style: cycle badge width fix , all cycle empty state fix and cycle icon size fix
This commit is contained in:
parent
c3d520aefd
commit
5916d6e749
@ -29,7 +29,12 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
||||
<span className="h-4 w-4 ">
|
||||
<ProgressBar value={completed} maxValue={total} />
|
||||
</span>
|
||||
<span className="w-8 text-right">{Math.floor((completed / total) * 100)}%</span>
|
||||
<span className="w-8 text-right">
|
||||
{isNaN(Math.floor((completed / total) * 100))
|
||||
? "0"
|
||||
: Math.floor((completed / total) * 100)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<span>of</span>
|
||||
<span>{total}</span>
|
||||
|
@ -51,6 +51,7 @@ import {
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_DRAFT_LIST,
|
||||
CYCLE_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
@ -153,6 +154,15 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
);
|
||||
break;
|
||||
}
|
||||
mutate(
|
||||
CYCLE_DETAILS(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
@ -213,6 +223,15 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
);
|
||||
break;
|
||||
}
|
||||
mutate(
|
||||
CYCLE_DETAILS(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
@ -251,24 +270,26 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
<div className="grid-row-2 grid rounded-[10px] shadow divide-y bg-brand-base border border-brand-base">
|
||||
<div className="grid grid-cols-1 divide-y border-brand-base lg:divide-y-0 lg:divide-x lg:grid-cols-3">
|
||||
<div className="flex flex-col text-xs">
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-5 rounded-b-[10px] p-4">
|
||||
<a className="h-full w-full">
|
||||
<div className="flex h-60 flex-col gap-5 justify-between rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<ContrastIcon
|
||||
className="h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "#858E96"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<span className="h-5 w-5">
|
||||
<ContrastIcon
|
||||
className="h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "#858E96"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
<Tooltip tooltipContent={cycle.name} position="top-left">
|
||||
<h3 className="break-all text-lg font-semibold">
|
||||
{truncateText(cycle.name, 70)}
|
||||
@ -291,17 +312,17 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1">
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<PersonRunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<AlarmClockIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1">
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
@ -400,7 +421,7 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
</a>
|
||||
</div>
|
||||
<div className="grid col-span-2 grid-cols-1 divide-y border-brand-base md:divide-y-0 md:divide-x md:grid-cols-2">
|
||||
<div className="flex h-full flex-col border-brand-base">
|
||||
<div className="flex h-60 flex-col border-brand-base">
|
||||
<div className="flex h-full w-full flex-col text-brand-secondary p-4">
|
||||
<div className="flex w-full items-center gap-2 py-1">
|
||||
<span>Progress</span>
|
||||
@ -428,7 +449,7 @@ export const ActiveCycleDetails: React.FC<TSingleStatProps> = ({ cycle, isComple
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-brand-base p-4">
|
||||
<div className="border-brand-base h-60 overflow-y-auto p-4">
|
||||
<ActiveCycleProgressStats issues={issues ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@ export const AllCyclesBoard: React.FC<TCycleStatsViewProps> = ({
|
||||
</div>
|
||||
) : type === "current" ? (
|
||||
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
|
||||
<h3 className="text-base font-medium text-brand-base ">No current cycle is present.</h3>
|
||||
<h3 className="text-base font-medium text-brand-base ">No cycle is present.</h3>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
|
@ -65,7 +65,7 @@ export const AllCyclesList: React.FC<TCycleStatsViewProps> = ({
|
||||
</div>
|
||||
) : type === "current" ? (
|
||||
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
|
||||
<h3 className="text-base font-medium text-brand-base ">No current cycle is present.</h3>
|
||||
<h3 className="text-base font-medium text-brand-base ">No cycle is present.</h3>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
|
@ -13,9 +13,10 @@ import {
|
||||
CompletedCycles,
|
||||
} from "components/cycles";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
import { EmptyState, Loader } from "components/ui";
|
||||
// icons
|
||||
import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import emptyCycle from "public/empty-state/empty-cycle.svg";
|
||||
// types
|
||||
import {
|
||||
SelectCycleType,
|
||||
@ -25,8 +26,6 @@ import {
|
||||
} from "types";
|
||||
|
||||
type Props = {
|
||||
cycleView: string;
|
||||
setCycleView: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
cyclesCompleteList: ICycle[] | undefined;
|
||||
@ -35,8 +34,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const CyclesView: React.FC<Props> = ({
|
||||
cycleView,
|
||||
setCycleView,
|
||||
setSelectedCycle,
|
||||
setCreateUpdateCycleModal,
|
||||
cyclesCompleteList,
|
||||
@ -44,6 +41,7 @@ export const CyclesView: React.FC<Props> = ({
|
||||
draftCycles,
|
||||
}) => {
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All");
|
||||
const { storedValue: cycleView, setValue: setCycleView } = useLocalStorage("cycleView", "list");
|
||||
|
||||
const currentTabValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
@ -153,8 +151,15 @@ export const CyclesView: React.FC<Props> = ({
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
{currentAndUpcomingCycles?.current_cycle?.[0] && (
|
||||
{currentAndUpcomingCycles?.current_cycle?.[0] ? (
|
||||
<ActiveCycleDetails cycle={currentAndUpcomingCycles?.current_cycle?.[0]} />
|
||||
) : (
|
||||
<EmptyState
|
||||
type="cycle"
|
||||
title="Create New Cycle"
|
||||
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
|
||||
imgURL={emptyCycle}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
@ -177,7 +182,7 @@ export const CyclesView: React.FC<Props> = ({
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||
<CompletedCycles
|
||||
cycleView={cycleView}
|
||||
cycleView={cycleView ?? "list"}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
/>
|
||||
|
@ -160,14 +160,10 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
title: "Error!",
|
||||
message: "Unable to create cycle in past date. Please enter a valid date.",
|
||||
});
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const isDateValid = await dateChecker({
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
});
|
||||
|
||||
if (data?.start_date && data?.end_date) {
|
||||
const isDateValidForExistingCycle = await dateChecker({
|
||||
start_date: payload.start_date,
|
||||
@ -185,10 +181,16 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
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",
|
||||
});
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isDateValid = await dateChecker({
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
});
|
||||
|
||||
if (isDateValid) {
|
||||
if (data) {
|
||||
await updateCycle(data.id, payload);
|
||||
@ -202,6 +204,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
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",
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
} else {
|
||||
if (data) {
|
||||
|
@ -8,7 +8,6 @@ import useSWR, { mutate } from "swr";
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||
import DatePicker from "react-datepicker";
|
||||
// icons
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
@ -21,7 +20,7 @@ import {
|
||||
LinkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// ui
|
||||
import { CustomMenu, Loader, ProgressBar } from "components/ui";
|
||||
import { CustomMenu, CustomRangeDatePicker, Loader, ProgressBar } from "components/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
@ -34,7 +33,11 @@ import { DeleteCycleModal } from "components/cycles";
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
// helpers
|
||||
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
|
||||
import { isDateRangeValid, renderDateFormat, renderShortDate } from "helpers/date-time.helper";
|
||||
import {
|
||||
isDateGreaterThanToday,
|
||||
renderDateFormat,
|
||||
renderShortDate,
|
||||
} from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycle, IIssue } from "types";
|
||||
// fetch-keys
|
||||
@ -77,7 +80,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const { reset, watch } = useForm({
|
||||
const { setValue, reset, watch } = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
@ -122,6 +125,166 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
});
|
||||
}, [cycle, reset]);
|
||||
|
||||
const dateChecker = async (payload: any) => {
|
||||
try {
|
||||
const res = await cyclesService.cycleDateCheck(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
payload
|
||||
);
|
||||
return res.status;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartDateChange = async (date: string) => {
|
||||
setValue("start_date", date);
|
||||
if (
|
||||
watch("start_date") &&
|
||||
watch("end_date") &&
|
||||
watch("start_date") !== "" &&
|
||||
watch("end_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.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (cycle?.start_date && cycle?.end_date) {
|
||||
const isDateValidForExistingCycle = await dateChecker({
|
||||
start_date: `${watch("start_date")}`,
|
||||
end_date: `${watch("end_date")}`,
|
||||
cycle_id: cycle.id,
|
||||
});
|
||||
|
||||
if (isDateValidForExistingCycle) {
|
||||
await submitChanges({
|
||||
start_date: renderDateFormat(`${watch("start_date")}`),
|
||||
end_date: renderDateFormat(`${watch("end_date")}`),
|
||||
});
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
return;
|
||||
} 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",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isDateValid = await dateChecker({
|
||||
start_date: `${watch("start_date")}`,
|
||||
end_date: `${watch("end_date")}`,
|
||||
});
|
||||
|
||||
if (isDateValid) {
|
||||
submitChanges({
|
||||
start_date: renderDateFormat(`${watch("start_date")}`),
|
||||
end_date: renderDateFormat(`${watch("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",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndDateChange = async (date: string) => {
|
||||
setValue("end_date", date);
|
||||
|
||||
if (
|
||||
watch("start_date") &&
|
||||
watch("end_date") &&
|
||||
watch("start_date") !== "" &&
|
||||
watch("end_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.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (cycle?.start_date && cycle?.end_date) {
|
||||
const isDateValidForExistingCycle = await dateChecker({
|
||||
start_date: `${watch("start_date")}`,
|
||||
end_date: `${watch("end_date")}`,
|
||||
cycle_id: cycle.id,
|
||||
});
|
||||
|
||||
if (isDateValidForExistingCycle) {
|
||||
await submitChanges({
|
||||
start_date: renderDateFormat(`${watch("start_date")}`),
|
||||
end_date: renderDateFormat(`${watch("end_date")}`),
|
||||
});
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
return;
|
||||
} 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",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isDateValid = await dateChecker({
|
||||
start_date: `${watch("start_date")}`,
|
||||
end_date: `${watch("end_date")}`,
|
||||
});
|
||||
|
||||
if (isDateValid) {
|
||||
submitChanges({
|
||||
start_date: renderDateFormat(`${watch("start_date")}`),
|
||||
end_date: renderDateFormat(`${watch("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",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isStartValid = new Date(`${cycle?.start_date}`) <= new Date();
|
||||
const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`);
|
||||
|
||||
@ -135,7 +298,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
<div
|
||||
className={`fixed top-[66px] ${
|
||||
isOpen ? "right-0" : "-right-[24rem]"
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-sidebar py-5 duration-300`}
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-sidebar pt-5 pb-10 duration-300`}
|
||||
>
|
||||
{cycle ? (
|
||||
<>
|
||||
@ -158,7 +321,12 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
>
|
||||
<CalendarDaysIcon className="h-3 w-3" />
|
||||
<span>
|
||||
{renderShortDate(new Date(`${cycle?.start_date}`), "Start date")}
|
||||
{renderShortDate(
|
||||
new Date(
|
||||
`${watch("start_date") ? watch("start_date") : cycle?.start_date}`
|
||||
),
|
||||
"Start date"
|
||||
)}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
|
||||
@ -172,36 +340,17 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||
<DatePicker
|
||||
selected={
|
||||
watch("start_date")
|
||||
? new Date(`${watch("start_date")}`)
|
||||
: new Date()
|
||||
}
|
||||
onChange={(date) => {
|
||||
if (date && watch("end_date")) {
|
||||
if (
|
||||
isDateRangeValid(renderDateFormat(date), `${watch("end_date")}`)
|
||||
) {
|
||||
submitChanges({
|
||||
start_date: renderDateFormat(date),
|
||||
});
|
||||
} else {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"The date you have entered is invalid. Please check and enter a valid date.",
|
||||
});
|
||||
}
|
||||
<CustomRangeDatePicker
|
||||
value={watch("start_date") ? watch("start_date") : cycle?.start_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
handleStartDateChange(val);
|
||||
}
|
||||
}}
|
||||
selectsStart
|
||||
startDate={new Date(`${watch("start_date")}`)}
|
||||
endDate={new Date(`${watch("end_date")}`)}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
maxDate={new Date(`${watch("end_date")}`)}
|
||||
shouldCloseOnSelect
|
||||
inline
|
||||
selectsStart
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
@ -222,7 +371,14 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
>
|
||||
<CalendarDaysIcon className="h-3 w-3" />
|
||||
|
||||
<span>{renderShortDate(new Date(`${cycle?.end_date}`), "End date")}</span>
|
||||
<span>
|
||||
{renderShortDate(
|
||||
new Date(
|
||||
`${watch("end_date") ? watch("end_date") : cycle?.end_date}`
|
||||
),
|
||||
"End date"
|
||||
)}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
@ -235,37 +391,17 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||
<DatePicker
|
||||
selected={
|
||||
watch("end_date") ? new Date(`${watch("end_date")}`) : new Date()
|
||||
}
|
||||
onChange={(date) => {
|
||||
if (watch("start_date") && date) {
|
||||
if (
|
||||
isDateRangeValid(
|
||||
`${watch("start_date")}`,
|
||||
renderDateFormat(date)
|
||||
)
|
||||
) {
|
||||
submitChanges({
|
||||
end_date: renderDateFormat(date),
|
||||
});
|
||||
} else {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"The date you have entered is invalid. Please check and enter a valid date.",
|
||||
});
|
||||
}
|
||||
<CustomRangeDatePicker
|
||||
value={watch("end_date") ? watch("end_date") : cycle?.end_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
handleEndDateChange(val);
|
||||
}
|
||||
}}
|
||||
selectsEnd
|
||||
startDate={new Date(`${watch("start_date")}`)}
|
||||
endDate={new Date(`${watch("end_date")}`)}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
minDate={new Date(`${watch("start_date")}`)}
|
||||
shouldCloseOnSelect
|
||||
inline
|
||||
selectsEnd
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
|
@ -51,6 +51,7 @@ import {
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_DRAFT_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
@ -150,6 +151,15 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
);
|
||||
break;
|
||||
}
|
||||
mutate(
|
||||
CYCLE_DETAILS(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
@ -210,6 +220,15 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
);
|
||||
break;
|
||||
}
|
||||
mutate(
|
||||
CYCLE_DETAILS(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
@ -263,20 +282,22 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<ContrastIcon
|
||||
className="h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "#858E96"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<span className="h-5 w-5">
|
||||
<ContrastIcon
|
||||
className="h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "#858E96"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
<Tooltip tooltipContent={cycle.name} className="break-all" position="top-left">
|
||||
<h3 className="break-all text-lg font-semibold">
|
||||
{truncateText(cycle.name, 15)}
|
||||
@ -299,17 +320,17 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1">
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<PersonRunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<AlarmClockIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1">
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
|
@ -42,6 +42,7 @@ import {
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_DRAFT_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { type } from "os";
|
||||
@ -184,6 +185,15 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
);
|
||||
break;
|
||||
}
|
||||
mutate(
|
||||
CYCLE_DETAILS(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
@ -244,6 +254,15 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
);
|
||||
break;
|
||||
}
|
||||
mutate(
|
||||
CYCLE_DETAILS(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
|
@ -185,7 +185,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ issues, module, isOpen,
|
||||
<div
|
||||
className={`fixed top-[66px] ${
|
||||
isOpen ? "right-0" : "-right-[24rem]"
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-sidebar py-5 duration-300`}
|
||||
} h-full w-[24rem] overflow-y-auto border-l border-brand-base bg-brand-sidebar pt-5 pb-10 duration-300`}
|
||||
>
|
||||
{module ? (
|
||||
<>
|
||||
|
@ -23,3 +23,5 @@ export * from "./tooltip";
|
||||
export * from "./toggle-switch";
|
||||
export * from "./markdown-to-component";
|
||||
export * from "./product-updates-modal";
|
||||
export * from "./integration-and-import-export-banner";
|
||||
export * from "./range-datepicker";
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
bannerName: string;
|
||||
};
|
||||
|
||||
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName }) => (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<h3 className="text-2xl font-semibold">{bannerName}</h3>
|
||||
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
|
||||
<ExclamationIcon height={24} width={24} className="fill-current text-brand-base" />
|
||||
<p className="leading-5">
|
||||
Integrations and importers are only available on the cloud version. We plan to open-source
|
||||
our SDKs in the near future so that the community can request or contribute integrations as
|
||||
needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
65
apps/app/components/ui/range-datepicker.tsx
Normal file
65
apps/app/components/ui/range-datepicker.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
// helpers
|
||||
import { renderDateFormat } 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(renderDateFormat(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-brand-surface-2"
|
||||
} duration-300 focus:border-brand-accent focus:outline-none focus:ring-1 focus:ring-brand-accent`
|
||||
: ""
|
||||
} ${error ? "border-red-500 bg-red-100" : ""} ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} w-full rounded-md border border-brand-base 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
|
||||
/>
|
||||
);
|
@ -46,11 +46,6 @@ export const Tooltip: React.FC<Props> = ({
|
||||
theme === "light" ? "text-brand-muted-1 bg-brand-surface-2" : "bg-black text-white"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 left-1/2 -top-1 h-3 w-3 rotate-45 bg-brand-surface-2 ${
|
||||
theme === "light" ? "text-brand-muted-1 bg-brand-surface-2" : "bg-black text-white"
|
||||
}`}
|
||||
/>
|
||||
{tooltipHeading && <h5 className="font-medium">{tooltipHeading}</h5>}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
|
@ -137,7 +137,7 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
|
||||
autoComplete="off"
|
||||
name="slug"
|
||||
register={register}
|
||||
className="block w-full rounded-md bg-transparent py-2 px-0 text-sm"
|
||||
className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm"
|
||||
validations={{
|
||||
required: "Workspace URL is required",
|
||||
}}
|
||||
|
@ -19,7 +19,7 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`z-30 h-full flex-shrink-0 border-r border-brand-base ${
|
||||
className={`z-20 h-full flex-shrink-0 border-r border-brand-base ${
|
||||
sidebarCollapse ? "" : "w-auto md:w-[17rem]"
|
||||
} fixed inset-y-0 top-0 ${
|
||||
toggleSidebar ? "left-0" : "-left-full md:left-0"
|
||||
|
@ -30,7 +30,6 @@ import {
|
||||
const ProjectCycles: NextPage = () => {
|
||||
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
const [cycleView, setCycleView] = useState<string>("list");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -121,8 +120,6 @@ const ProjectCycles: NextPage = () => {
|
||||
<div className="flex flex-col gap-5">
|
||||
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
|
||||
<CyclesView
|
||||
cycleView={cycleView}
|
||||
setCycleView={setCycleView}
|
||||
setSelectedCycle={setSelectedCycle}
|
||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||
cyclesCompleteList={cyclesCompleteList}
|
||||
|
@ -12,11 +12,15 @@ import projectService from "services/project.service";
|
||||
// components
|
||||
import { SettingsHeader, SingleIntegration } from "components/project";
|
||||
// ui
|
||||
import { EmptySpace, EmptySpaceItem, Loader } from "components/ui";
|
||||
import {
|
||||
EmptySpace,
|
||||
EmptySpaceItem,
|
||||
IntegrationAndImportExportBanner,
|
||||
Loader,
|
||||
} from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline";
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
import type { NextPage } from "next";
|
||||
@ -59,21 +63,7 @@ const ProjectIntegrations: NextPage = () => {
|
||||
{workspaceIntegrations ? (
|
||||
workspaceIntegrations.length > 0 ? (
|
||||
<section className="space-y-8">
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<h3 className="text-2xl font-semibold">Integrations</h3>
|
||||
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
|
||||
<ExclamationIcon
|
||||
height={24}
|
||||
width={24}
|
||||
className="fill-current text-brand-base"
|
||||
/>
|
||||
<p className="leading-5">
|
||||
Integrations and importers are only available on the cloud version. We plan to
|
||||
open-source our SDKs in the near future so that the community can request or
|
||||
contribute integrations as needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
<div className="space-y-5">
|
||||
{workspaceIntegrations.map((integration) => (
|
||||
<SingleIntegration
|
||||
|
@ -9,6 +9,7 @@ import IntegrationGuide from "components/integration/guide";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import { IntegrationAndImportExportBanner } from "components/ui";
|
||||
|
||||
const ImportExport: NextPage = () => {
|
||||
const router = useRouter();
|
||||
@ -23,8 +24,9 @@ const ImportExport: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="p-8">
|
||||
<div className="p-8 space-y-4">
|
||||
<SettingsHeader />
|
||||
<IntegrationAndImportExportBanner bannerName="Import/ Export" />
|
||||
<IntegrationGuide />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
|
@ -13,10 +13,8 @@ import { SettingsHeader } from "components/workspace";
|
||||
// components
|
||||
import { SingleIntegrationCard } from "components/integration";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
import { IntegrationAndImportExportBanner, Loader } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
@ -50,17 +48,7 @@ const WorkspaceIntegrations: NextPage = () => {
|
||||
<div className="p-8">
|
||||
<SettingsHeader />
|
||||
<section className="space-y-5">
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<h3 className="text-2xl font-semibold">Integrations</h3>
|
||||
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
|
||||
<ExclamationIcon height={24} width={24} className="fill-current text-brand-base" />
|
||||
<p className="leading-5">
|
||||
Integrations and importers are only available on the cloud version. We plan to
|
||||
open-source our SDKs in the near future so that the community can request or
|
||||
contribute integrations as needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
<div className="space-y-5">
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => (
|
||||
|
@ -117,6 +117,15 @@
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day--in-range {
|
||||
.react-datepicker__day--in-range-start,
|
||||
.react-datepicker__day--in-range-end {
|
||||
background-color: rgba(var(--color-bg-accent)) !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-accent)) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user