forked from github/plane
Merge pull request #358 from makeplane/feat/cycle_validations
feat: cycle validations
This commit is contained in:
commit
f290a417bc
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";
|
type: "current" | "upcoming" | "completed";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CyclesListView: React.FC<TCycleStatsViewProps> = ({
|
export const CyclesList: React.FC<TCycleStatsViewProps> = ({
|
||||||
cycles,
|
cycles,
|
||||||
setCreateUpdateCycleModal,
|
setCreateUpdateCycleModal,
|
||||||
setSelectedCycle,
|
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
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui";
|
import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import { ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
|
// services
|
||||||
|
import cyclesService from "services/cycles.service";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
||||||
@ -17,17 +22,24 @@ type Props = {
|
|||||||
const defaultValues: Partial<ICycle> = {
|
const defaultValues: Partial<ICycle> = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
status: "draft",
|
start_date: null,
|
||||||
start_date: "",
|
end_date: null,
|
||||||
end_date: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
|
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 {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
|
watch,
|
||||||
reset,
|
reset,
|
||||||
} = useForm<ICycle>({
|
} = useForm<ICycle>({
|
||||||
defaultValues,
|
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(() => {
|
useEffect(() => {
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
@ -84,30 +121,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
register={register}
|
register={register}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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="flex gap-x-2">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<h6 className="text-gray-500">Start Date</h6>
|
<h6 className="text-gray-500">Start Date</h6>
|
||||||
@ -115,12 +129,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="start_date"
|
name="start_date"
|
||||||
rules={{ required: "Start date is required" }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
renderAs="input"
|
renderAs="input"
|
||||||
value={value}
|
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}
|
error={errors.start_date ? true : false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -136,12 +157,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="end_date"
|
name="end_date"
|
||||||
rules={{ required: "End date is required" }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
renderAs="input"
|
renderAs="input"
|
||||||
value={value}
|
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}
|
error={errors.end_date ? true : false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -158,7 +186,18 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
|||||||
<Button theme="secondary" onClick={handleClose}>
|
<Button theme="secondary" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</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
|
{status
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
? "Updating Cycle..."
|
? "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 "./delete-cycle-modal";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
|
@ -12,10 +12,16 @@ import cycleService from "services/cycles.service";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CycleForm } from "components/cycles";
|
import { CycleForm } from "components/cycles";
|
||||||
|
// helper
|
||||||
|
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import type { ICycle } from "types";
|
import type { ICycle } from "types";
|
||||||
// fetch keys
|
// 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 = {
|
type CycleModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -37,7 +43,23 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||||||
await cycleService
|
await cycleService
|
||||||
.createCycle(workspaceSlug as string, projectId as string, payload)
|
.createCycle(workspaceSlug as string, projectId as string, payload)
|
||||||
.then((res) => {
|
.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();
|
handleClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -59,7 +81,23 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||||||
await cycleService
|
await cycleService
|
||||||
.updateCycle(workspaceSlug as string, projectId as string, cycleId, payload)
|
.updateCycle(workspaceSlug as string, projectId as string, cycleId, payload)
|
||||||
.then((res) => {
|
.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();
|
handleClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -113,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
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
|
<CycleForm
|
||||||
handleFormSubmit={handleFormSubmit}
|
handleFormSubmit={handleFormSubmit}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
|
@ -6,7 +6,7 @@ import Image from "next/image";
|
|||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
// icons
|
// icons
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// ui
|
// ui
|
||||||
import { CustomSelect, Loader, ProgressBar } from "components/ui";
|
import { Loader, ProgressBar } from "components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
@ -36,17 +36,22 @@ import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-tim
|
|||||||
import { CycleIssueResponse, ICycle, IIssue } from "types";
|
import { CycleIssueResponse, ICycle, IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
||||||
// constants
|
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: IIssue[];
|
issues: IIssue[];
|
||||||
cycle: ICycle | undefined;
|
cycle: ICycle | undefined;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
cycleIssues: CycleIssueResponse[];
|
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 [cycleDeleteModal, setCycleDeleteModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -60,7 +65,6 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
|
|||||||
const defaultValues: Partial<ICycle> = {
|
const defaultValues: Partial<ICycle> = {
|
||||||
start_date: new Date().toString(),
|
start_date: new Date().toString(),
|
||||||
end_date: new Date().toString(),
|
end_date: new Date().toString(),
|
||||||
status: cycle?.status,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupedIssues = {
|
const groupedIssues = {
|
||||||
@ -72,7 +76,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
|
|||||||
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
|
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { reset, watch, control } = useForm({
|
const { reset } = useForm({
|
||||||
defaultValues,
|
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 gap-1 text-sm my-2">
|
||||||
<div className="flex items-center ">
|
<div className="flex items-center ">
|
||||||
<Controller
|
<span
|
||||||
control={control}
|
className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`}
|
||||||
name="status"
|
>
|
||||||
render={({ field: { value } }) => (
|
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||||
<CustomSelect
|
{cycleStatus === "current"
|
||||||
label={
|
? "In Progress"
|
||||||
<span
|
: cycleStatus === "completed"
|
||||||
className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`}
|
? "Completed"
|
||||||
>
|
: cycleStatus === "upcoming"
|
||||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
? "Upcoming"
|
||||||
{watch("status")}
|
: "Draft"}
|
||||||
</span>
|
</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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</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">
|
<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">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
|
<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 &&
|
||||||
? cycle.owned_by.first_name.charAt(0)
|
cycle.owned_by?.first_name &&
|
||||||
|
cycle.owned_by?.first_name !== ""
|
||||||
|
? cycle.owned_by?.first_name.charAt(0)
|
||||||
: cycle.owned_by?.email.charAt(0)}
|
: cycle.owned_by?.email.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{cycle.owned_by.first_name !== ""
|
{cycle.owned_by?.first_name !== ""
|
||||||
? cycle.owned_by.first_name
|
? cycle.owned_by?.first_name
|
||||||
: cycle.owned_by.email}
|
: cycle.owned_by?.email}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export const CYCLE_STATUS = [
|
|
||||||
{ label: "Started", value: "started", color: "#5e6ad2" },
|
|
||||||
{ label: "Completed", value: "completed", color: "#eb5757" },
|
|
||||||
{ label: "Draft", value: "draft", color: "#f2c94c" },
|
|
||||||
];
|
|
@ -35,6 +35,9 @@ export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
|
|||||||
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
|
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
|
||||||
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;
|
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;
|
||||||
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAIL_${cycleId}`;
|
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAIL_${cycleId}`;
|
||||||
|
export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) => `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`;
|
||||||
|
export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId}`;
|
||||||
|
export const CYCLE_COMPLETE_LIST = (projectId: string) => `CYCLE_COMPLETE_LIST_${projectId}`;
|
||||||
|
|
||||||
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
|
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
|
||||||
export const STATE_DETAIL = "STATE_DETAIL";
|
export const STATE_DETAIL = "STATE_DETAIL";
|
||||||
|
@ -88,3 +88,17 @@ export const timeAgo = (time: any) => {
|
|||||||
}
|
}
|
||||||
return time;
|
return time;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDateRangeStatus = (startDate: string , endDate: string ) => {
|
||||||
|
const now = new Date();
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const end = new Date(endDate);
|
||||||
|
|
||||||
|
if (end < now) {
|
||||||
|
return "completed";
|
||||||
|
} else if (start <= now && end >= now) {
|
||||||
|
return "current";
|
||||||
|
} else {
|
||||||
|
return "upcoming";
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
|
|||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { CycleIssueResponse, UserAuth } from "types";
|
import { CycleIssueResponse, UserAuth } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -78,6 +79,11 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cycleStatus =
|
||||||
|
cycleDetails?.start_date && cycleDetails?.end_date
|
||||||
|
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||||
|
: "draft";
|
||||||
|
|
||||||
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
||||||
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
||||||
workspaceSlug && projectId && cycleId
|
workspaceSlug && projectId && cycleId
|
||||||
@ -218,6 +224,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CycleDetailsSidebar
|
<CycleDetailsSidebar
|
||||||
|
cycleStatus={cycleStatus}
|
||||||
issues={cycleIssuesArray ?? []}
|
issues={cycleIssuesArray ?? []}
|
||||||
cycle={cycleDetails}
|
cycle={cycleDetails}
|
||||||
isOpen={cycleSidebar}
|
isOpen={cycleSidebar}
|
||||||
|
@ -1,30 +1,47 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
// lib
|
// lib
|
||||||
import { requiredAuth } from "lib/auth";
|
import { requiredAuth } from "lib/auth";
|
||||||
import { CyclesIcon } from "components/icons";
|
|
||||||
// services
|
// services
|
||||||
import cycleService from "services/cycles.service";
|
import cycleService from "services/cycles.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import workspaceService from "services/workspace.service";
|
|
||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateCycleModal, CyclesListView } from "components/cycles";
|
import { CompletedCyclesListProps, CreateUpdateCycleModal, CyclesList } from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { HeaderButton, EmptySpace, EmptySpaceItem, Loader } from "components/ui";
|
import { HeaderButton, Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
// types
|
// types
|
||||||
import { ICycle, SelectCycleType } from "types";
|
import { SelectCycleType } from "types";
|
||||||
import type { NextPage, GetServerSidePropsContext } from "next";
|
import type { NextPage, GetServerSidePropsContext } from "next";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
import { CYCLE_LIST, PROJECT_DETAILS, WORKSPACE_DETAILS } from "constants/fetch-keys";
|
import {
|
||||||
|
CYCLE_CURRENT_AND_UPCOMING_LIST,
|
||||||
|
CYCLE_DRAFT_LIST,
|
||||||
|
PROJECT_DETAILS,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const CompletedCyclesList = dynamic<CompletedCyclesListProps>(
|
||||||
|
() => import("components/cycles").then((a) => a.CompletedCyclesList),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<Loader className="mb-5">
|
||||||
|
<Loader.Item height="12rem" width="100%" />
|
||||||
|
</Loader>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const ProjectCycles: NextPage = () => {
|
const ProjectCycles: NextPage = () => {
|
||||||
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
||||||
@ -34,43 +51,25 @@ const ProjectCycles: NextPage = () => {
|
|||||||
query: { workspaceSlug, projectId },
|
query: { workspaceSlug, projectId },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
|
|
||||||
const { data: activeWorkspace } = useSWR(
|
|
||||||
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
|
||||||
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: activeProject } = useSWR(
|
const { data: activeProject } = useSWR(
|
||||||
activeWorkspace && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
activeWorkspace && projectId
|
workspaceSlug && projectId
|
||||||
? () => projectService.getProject(activeWorkspace.slug, projectId as string)
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: cycles } = useSWR<ICycle[]>(
|
const { data: draftCycles } = useSWR(
|
||||||
activeWorkspace && projectId ? CYCLE_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null,
|
||||||
activeWorkspace && projectId
|
workspaceSlug && projectId
|
||||||
? () => cycleService.getCycles(activeWorkspace.slug, projectId as string)
|
? () => cycleService.getDraftCycles(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCycleStatus = (startDate: string, endDate: string) => {
|
const { data: currentAndUpcomingCycles } = useSWR(
|
||||||
const today = new Date();
|
workspaceSlug && projectId ? CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
if (today < new Date(startDate)) return "upcoming";
|
? () => cycleService.getCurrentAndUpcomingCycles(workspaceSlug as string, projectId as string)
|
||||||
else if (today > new Date(endDate)) return "completed";
|
: null
|
||||||
else return "current";
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentCycles = cycles?.filter(
|
|
||||||
(c) => getCycleStatus(c.start_date, c.end_date) === "current"
|
|
||||||
);
|
|
||||||
|
|
||||||
const upcomingCycles = cycles?.filter(
|
|
||||||
(c) => getCycleStatus(c.start_date, c.end_date) === "upcoming"
|
|
||||||
);
|
|
||||||
|
|
||||||
const completedCycles = cycles?.filter(
|
|
||||||
(c) => getCycleStatus(c.start_date, c.end_date) === "completed"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -110,92 +109,71 @@ const ProjectCycles: NextPage = () => {
|
|||||||
handleClose={() => setCreateUpdateCycleModal(false)}
|
handleClose={() => setCreateUpdateCycleModal(false)}
|
||||||
data={selectedCycle}
|
data={selectedCycle}
|
||||||
/>
|
/>
|
||||||
{cycles ? (
|
<div className="space-y-8">
|
||||||
cycles.length > 0 ? (
|
<h3 className="text-xl font-medium leading-6 text-gray-900">Current Cycle</h3>
|
||||||
<div className="space-y-8">
|
<div className="space-y-5">
|
||||||
<h3 className="text-xl font-medium leading-6 text-gray-900">Current Cycle</h3>
|
<CyclesList
|
||||||
<div className="space-y-5">
|
cycles={currentAndUpcomingCycles?.current_cycle ?? []}
|
||||||
<CyclesListView
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
cycles={currentCycles ?? []}
|
setSelectedCycle={setSelectedCycle}
|
||||||
|
type="current"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List
|
||||||
|
as="div"
|
||||||
|
className="flex justify-between items-center gap-2 rounded-lg bg-gray-100 p-2 text-sm"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
className={({ selected }) =>
|
||||||
|
`w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Upcoming
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({ selected }) =>
|
||||||
|
`w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Completed
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({ selected }) =>
|
||||||
|
` w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Draft
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel as="div" className="mt-8 space-y-5">
|
||||||
|
<CyclesList
|
||||||
|
cycles={currentAndUpcomingCycles?.upcoming_cycle ?? []}
|
||||||
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
|
setSelectedCycle={setSelectedCycle}
|
||||||
|
type="upcoming"
|
||||||
|
/>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel as="div" className="mt-8 space-y-5">
|
||||||
|
<CompletedCyclesList
|
||||||
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
|
setSelectedCycle={setSelectedCycle}
|
||||||
|
/>
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
<Tab.Panel as="div" className="mt-8 space-y-5">
|
||||||
|
<CyclesList
|
||||||
|
cycles={draftCycles?.draft_cycles ?? []}
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
setSelectedCycle={setSelectedCycle}
|
setSelectedCycle={setSelectedCycle}
|
||||||
type="current"
|
type="upcoming"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Tab.Panel>
|
||||||
<div className="space-y-5">
|
</Tab.Group>
|
||||||
<Tab.Group>
|
</div>
|
||||||
<Tab.List
|
</div>
|
||||||
as="div"
|
|
||||||
className="grid grid-cols-2 items-center gap-2 rounded-lg bg-gray-100 p-2 text-sm"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
`rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Upcoming
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
`rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Completed
|
|
||||||
</Tab>
|
|
||||||
</Tab.List>
|
|
||||||
<Tab.Panels>
|
|
||||||
<Tab.Panel as="div" className="mt-8 space-y-5">
|
|
||||||
<CyclesListView
|
|
||||||
cycles={upcomingCycles ?? []}
|
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
|
||||||
setSelectedCycle={setSelectedCycle}
|
|
||||||
type="upcoming"
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel as="div" className="mt-8 space-y-5">
|
|
||||||
<CyclesListView
|
|
||||||
cycles={completedCycles ?? []}
|
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
|
||||||
setSelectedCycle={setSelectedCycle}
|
|
||||||
type="completed"
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center px-4">
|
|
||||||
<EmptySpace
|
|
||||||
title="You don't have any cycle yet."
|
|
||||||
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
|
|
||||||
Icon={CyclesIcon}
|
|
||||||
>
|
|
||||||
<EmptySpaceItem
|
|
||||||
title="Create a new cycle"
|
|
||||||
description={
|
|
||||||
<span>
|
|
||||||
Use <pre className="inline rounded bg-gray-200 px-2 py-1">Q</pre> shortcut to
|
|
||||||
create a new cycle
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
Icon={PlusIcon}
|
|
||||||
action={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", {
|
|
||||||
key: "q",
|
|
||||||
});
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EmptySpace>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Loader className="space-y-5">
|
|
||||||
<Loader.Item height="150px" />
|
|
||||||
<Loader.Item height="150px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
// types
|
// types
|
||||||
import type { ICycle } from "types";
|
import type { CompletedCyclesResponse, CurrentAndUpcomingCyclesResponse, DraftCyclesResponse, ICycle } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
@ -87,6 +87,47 @@ class ProjectCycleServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cycleDateCheck(workspaceSlug: string, projectId: string, data: {
|
||||||
|
start_date: string,
|
||||||
|
end_date: string
|
||||||
|
}): Promise<any> {
|
||||||
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise<CurrentAndUpcomingCyclesResponse> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDraftCycles(workspaceSlug: string, projectId: string): Promise<DraftCyclesResponse> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCompletedCycles(workspaceSlug: string, projectId: string): Promise<CompletedCyclesResponse> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ProjectCycleServices();
|
export default new ProjectCycleServices();
|
||||||
|
22
apps/app/types/cycles.d.ts
vendored
22
apps/app/types/cycles.d.ts
vendored
@ -7,16 +7,32 @@ export interface ICycle {
|
|||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
start_date: string;
|
start_date: string | null;
|
||||||
end_date: string;
|
end_date: string | null;
|
||||||
status: string;
|
|
||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
project: string;
|
project: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
issue: string;
|
issue: string;
|
||||||
|
current_cycle: [];
|
||||||
|
upcoming_cycle: [];
|
||||||
|
past_cycles: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CurrentAndUpcomingCyclesResponse {
|
||||||
|
current_cycle : ICycle[];
|
||||||
|
upcoming_cycle : ICycle[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface DraftCyclesResponse {
|
||||||
|
draft_cycles : ICycle[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompletedCyclesResponse {
|
||||||
|
completed_cycles : ICycle[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface CycleIssueResponse {
|
export interface CycleIssueResponse {
|
||||||
id: string;
|
id: string;
|
||||||
issue_detail: IIssue;
|
issue_detail: IIssue;
|
||||||
|
Loading…
Reference in New Issue
Block a user