Merge pull request #358 from makeplane/feat/cycle_validations
feat: cycle validations
This commit is contained in:
commit
f290a417bc
13 changed files with 411 additions and 205 deletions
82
apps/app/components/cycles/completed-cycles-list.tsx
Normal file
82
apps/app/components/cycles/completed-cycles-list.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// react
|
||||
import { useState } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// components
|
||||
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
|
||||
// types
|
||||
import { ICycle, SelectCycleType } from "types";
|
||||
import { CompletedCycleIcon } from "components/icons";
|
||||
import cyclesService from "services/cycles.service";
|
||||
import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys";
|
||||
|
||||
export interface CompletedCyclesListProps {
|
||||
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
|
||||
}
|
||||
|
||||
export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: completedCycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
|
||||
setCycleDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycle({ ...cycle, actionType: "edit" });
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{completedCycles && (
|
||||
<>
|
||||
<DeleteCycleModal
|
||||
isOpen={
|
||||
cycleDeleteModal &&
|
||||
!!selectedCycleForDelete &&
|
||||
selectedCycleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setCycleDeleteModal}
|
||||
data={selectedCycleForDelete}
|
||||
/>
|
||||
{completedCycles?.completed_cycles.length > 0 ? (
|
||||
completedCycles.completed_cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 text-center">
|
||||
<CompletedCycleIcon height="56" width="56" />
|
||||
<h3 className="text-gray-500">
|
||||
No completed cycles yet. Create with{" "}
|
||||
<pre className="inline rounded bg-gray-200 px-2 py-1">Q</pre>.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -13,7 +13,7 @@ type TCycleStatsViewProps = {
|
|||
type: "current" | "upcoming" | "completed";
|
||||
};
|
||||
|
||||
export const CyclesListView: React.FC<TCycleStatsViewProps> = ({
|
||||
export const CyclesList: React.FC<TCycleStatsViewProps> = ({
|
||||
cycles,
|
||||
setCreateUpdateCycleModal,
|
||||
setSelectedCycle,
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// toast
|
||||
import useToast from "hooks/use-toast";
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
|
||||
type Props = {
|
||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
||||
|
|
@ -17,17 +22,24 @@ type Props = {
|
|||
const defaultValues: Partial<ICycle> = {
|
||||
name: "",
|
||||
description: "",
|
||||
status: "draft",
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
};
|
||||
|
||||
export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const [isDateValid, setIsDateValid] = useState(true);
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
} = useForm<ICycle>({
|
||||
defaultValues,
|
||||
|
|
@ -41,6 +53,31 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||
});
|
||||
};
|
||||
|
||||
const dateChecker = async (payload: any) => {
|
||||
await cyclesService
|
||||
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
|
||||
.then((res) => {
|
||||
if (res.status) {
|
||||
setIsDateValid(true);
|
||||
} else {
|
||||
setIsDateValid(false);
|
||||
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",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const checkEmptyDate =
|
||||
(watch("start_date") === "" && watch("end_date") === "") ||
|
||||
(!watch("start_date") && !watch("end_date"));
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
|
|
@ -84,30 +121,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||
register={register}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-gray-500">Status</h6>
|
||||
<Controller
|
||||
name="status"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CustomSelect
|
||||
{...field}
|
||||
label={<span className="capitalize">{field.value ?? "Select Status"}</span>}
|
||||
input
|
||||
>
|
||||
{[
|
||||
{ label: "Draft", value: "draft" },
|
||||
{ label: "Started", value: "started" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
].map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
<div className="w-full">
|
||||
<h6 className="text-gray-500">Start Date</h6>
|
||||
|
|
@ -115,12 +129,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
rules={{ required: "Start date is required" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomDatePicker
|
||||
renderAs="input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
watch("end_date")
|
||||
? dateChecker({
|
||||
start_date: val,
|
||||
end_date: watch("end_date"),
|
||||
})
|
||||
: "";
|
||||
}}
|
||||
error={errors.start_date ? true : false}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -136,12 +157,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||
<Controller
|
||||
control={control}
|
||||
name="end_date"
|
||||
rules={{ required: "End date is required" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomDatePicker
|
||||
renderAs="input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
watch("start_date")
|
||||
? dateChecker({
|
||||
start_date: watch("start_date"),
|
||||
end_date: val,
|
||||
})
|
||||
: "";
|
||||
}}
|
||||
error={errors.end_date ? true : false}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -158,7 +186,18 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||
<Button theme="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className={
|
||||
checkEmptyDate
|
||||
? "cursor-pointer"
|
||||
: isDateValid
|
||||
? "cursor-pointer"
|
||||
: "cursor-not-allowed"
|
||||
}
|
||||
disabled={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true}
|
||||
>
|
||||
{status
|
||||
? isSubmitting
|
||||
? "Updating Cycle..."
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./cycles-list-view";
|
||||
export * from "./completed-cycles-list";
|
||||
export * from "./cycles-list";
|
||||
export * from "./delete-cycle-modal";
|
||||
export * from "./form";
|
||||
export * from "./modal";
|
||||
|
|
|
|||
|
|
@ -12,10 +12,16 @@ import cycleService from "services/cycles.service";
|
|||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CycleForm } from "components/cycles";
|
||||
// helper
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { ICycle } from "types";
|
||||
// fetch keys
|
||||
import { CYCLE_LIST } from "constants/fetch-keys";
|
||||
import {
|
||||
CYCLE_COMPLETE_LIST,
|
||||
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||
CYCLE_DRAFT_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type CycleModalProps = {
|
||||
isOpen: boolean;
|
||||
|
|
@ -37,7 +43,23 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||
await cycleService
|
||||
.createCycle(workspaceSlug as string, projectId as string, payload)
|
||||
.then((res) => {
|
||||
mutate(CYCLE_LIST(projectId as string));
|
||||
switch (
|
||||
res?.start_date && res.end_date
|
||||
? getDateRangeStatus(res?.start_date, res.end_date)
|
||||
: "draft"
|
||||
) {
|
||||
case "completed":
|
||||
mutate(CYCLE_COMPLETE_LIST(projectId as string));
|
||||
break;
|
||||
case "current":
|
||||
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
|
||||
break;
|
||||
case "upcoming":
|
||||
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
|
||||
break;
|
||||
default:
|
||||
mutate(CYCLE_DRAFT_LIST(projectId as string));
|
||||
}
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
|
|
@ -59,7 +81,23 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||
await cycleService
|
||||
.updateCycle(workspaceSlug as string, projectId as string, cycleId, payload)
|
||||
.then((res) => {
|
||||
mutate(CYCLE_LIST(projectId as string));
|
||||
switch (
|
||||
res?.start_date && res.end_date
|
||||
? getDateRangeStatus(res?.start_date, res.end_date)
|
||||
: "draft"
|
||||
) {
|
||||
case "completed":
|
||||
mutate(CYCLE_COMPLETE_LIST(projectId as string));
|
||||
break;
|
||||
case "current":
|
||||
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
|
||||
break;
|
||||
case "upcoming":
|
||||
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
|
||||
break;
|
||||
default:
|
||||
mutate(CYCLE_DRAFT_LIST(projectId as string));
|
||||
}
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
|
|
@ -113,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<CycleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Image from "next/image";
|
|||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import DatePicker from "react-datepicker";
|
||||
// icons
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
UserIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// ui
|
||||
import { CustomSelect, Loader, ProgressBar } from "components/ui";
|
||||
import { Loader, ProgressBar } from "components/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
|
|
@ -36,17 +36,22 @@ import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-tim
|
|||
import { CycleIssueResponse, ICycle, IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
|
||||
type Props = {
|
||||
issues: IIssue[];
|
||||
cycle: ICycle | undefined;
|
||||
isOpen: boolean;
|
||||
cycleIssues: CycleIssueResponse[];
|
||||
cycleStatus: string;
|
||||
};
|
||||
|
||||
export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cycleIssues }) => {
|
||||
export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
issues,
|
||||
cycle,
|
||||
isOpen,
|
||||
cycleIssues,
|
||||
cycleStatus,
|
||||
}) => {
|
||||
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
|
@ -60,7 +65,6 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
|
|||
const defaultValues: Partial<ICycle> = {
|
||||
start_date: new Date().toString(),
|
||||
end_date: new Date().toString(),
|
||||
status: cycle?.status,
|
||||
};
|
||||
|
||||
const groupedIssues = {
|
||||
|
|
@ -72,7 +76,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
|
|||
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
|
||||
};
|
||||
|
||||
const { reset, watch, control } = useForm({
|
||||
const { reset } = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
|
|
@ -118,32 +122,18 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
|
|||
<>
|
||||
<div className="flex gap-1 text-sm my-2">
|
||||
<div className="flex items-center ">
|
||||
<Controller
|
||||
control={control}
|
||||
name="status"
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
label={
|
||||
<span
|
||||
className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{watch("status")}
|
||||
</span>
|
||||
}
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ status: value });
|
||||
}}
|
||||
>
|
||||
{CYCLE_STATUS.map((option) => (
|
||||
<CustomSelect.Option key={option.value} value={option.value}>
|
||||
<span className="text-xs">{option.label}</span>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{cycleStatus === "current"
|
||||
? "In Progress"
|
||||
: cycleStatus === "completed"
|
||||
? "Completed"
|
||||
: cycleStatus === "upcoming"
|
||||
? "Upcoming"
|
||||
: "Draft"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-2 rounded-md border bg-transparent h-full p-2 px-4 text-xs font-medium text-gray-900 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
|
||||
<Popover className="flex justify-center items-center relative rounded-lg">
|
||||
|
|
@ -289,14 +279,16 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
|
|||
</div>
|
||||
) : (
|
||||
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
|
||||
{cycle.owned_by?.first_name && cycle.owned_by.first_name !== ""
|
||||
? cycle.owned_by.first_name.charAt(0)
|
||||
{cycle.owned_by &&
|
||||
cycle.owned_by?.first_name &&
|
||||
cycle.owned_by?.first_name !== ""
|
||||
? cycle.owned_by?.first_name.charAt(0)
|
||||
: cycle.owned_by?.email.charAt(0)}
|
||||
</div>
|
||||
))}
|
||||
{cycle.owned_by.first_name !== ""
|
||||
? cycle.owned_by.first_name
|
||||
: cycle.owned_by.email}
|
||||
{cycle.owned_by?.first_name !== ""
|
||||
? cycle.owned_by?.first_name
|
||||
: cycle.owned_by?.email}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue