mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
cycles changes
This commit is contained in:
parent
3bf590b67e
commit
2643de80af
@ -20,8 +20,7 @@ export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
|
||||
const { data: allCyclesList, mutate } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all")
|
||||
? () => cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all")
|
||||
: null
|
||||
);
|
||||
|
30
web/components/cycles/cycles-list.tsx
Normal file
30
web/components/cycles/cycles-list.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { CyclesView } from "./cycles-view";
|
||||
|
||||
export interface ICyclesList {
|
||||
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
|
||||
}
|
||||
|
||||
export const CyclesList: FC<ICyclesList> = (props) => {
|
||||
const { filter } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `CYCLES_LIST_${projectId}` : null,
|
||||
workspaceSlug && projectId
|
||||
? () => cycleStore.fetchCycles(workspaceSlug.toString(), projectId.toString(), filter)
|
||||
: null
|
||||
);
|
||||
if (!projectId) {
|
||||
return <></>;
|
||||
}
|
||||
return <CyclesView cycles={cycleStore.cycles[projectId?.toString()]} viewType={filter} />;
|
||||
};
|
@ -32,10 +32,11 @@ import {
|
||||
DRAFT_CYCLES_LIST,
|
||||
UPCOMING_CYCLES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle";
|
||||
|
||||
type Props = {
|
||||
cycles: ICycle[] | undefined;
|
||||
mutateCycles: KeyedMutator<ICycle[]>;
|
||||
mutateCycles?: KeyedMutator<ICycle[]>;
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
@ -46,7 +47,8 @@ export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType })
|
||||
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
|
||||
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
|
||||
|
||||
const { storedValue: cycleTab } = useLocalStorage("cycleTab", "All");
|
||||
const { storedValue: cycleTab } = useLocalStorage("cycle_tab", "all");
|
||||
console.log("cycleTab", cycleTab);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -145,15 +147,13 @@ export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType })
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
cyclesService.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -209,20 +209,8 @@ export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType })
|
||||
<div className="h-full grid place-items-center text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="mx-auto flex justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="66"
|
||||
height="66"
|
||||
viewBox="0 0 66 66"
|
||||
fill="none"
|
||||
>
|
||||
<circle
|
||||
cx="34.375"
|
||||
cy="34.375"
|
||||
r="22"
|
||||
stroke="rgb(var(--color-text-400))"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
|
||||
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
fill="rgb(var(--color-text-400))"
|
||||
@ -230,9 +218,7 @@ export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType })
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-sm text-custom-text-200">
|
||||
{cycleTab === "All"
|
||||
? "No cycles"
|
||||
: `No ${cycleTab === "Drafts" ? "draft" : cycleTab?.toLowerCase()} cycles`}
|
||||
{cycleTab === "all" ? "No cycles" : `No ${cycleTab} cycles`}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -17,7 +17,7 @@ import { ICycle } from "types";
|
||||
|
||||
type Props = {
|
||||
cycles: ICycle[];
|
||||
mutateCycles: KeyedMutator<ICycle[]>;
|
||||
mutateCycles?: KeyedMutator<ICycle[]>;
|
||||
};
|
||||
|
||||
export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) => {
|
||||
@ -29,33 +29,32 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
|
||||
|
||||
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !user) return;
|
||||
mutateCycles &&
|
||||
mutateCycles((prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
mutateCycles((prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
const newList = prevData.map((p: any) => ({
|
||||
...p,
|
||||
...(p.id === cycle.id
|
||||
? {
|
||||
start_date: payload.start_date ? payload.start_date : p.start_date,
|
||||
target_date: payload.target_date ? payload.target_date : p.end_date,
|
||||
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
|
||||
const newList = prevData.map((p: any) => ({
|
||||
...p,
|
||||
...(p.id === cycle.id
|
||||
? {
|
||||
start_date: payload.start_date ? payload.start_date : p.start_date,
|
||||
target_date: payload.target_date ? payload.target_date : p.end_date,
|
||||
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
if (payload.sort_order) {
|
||||
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||
}
|
||||
|
||||
if (payload.sort_order) {
|
||||
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||
}
|
||||
|
||||
return newList;
|
||||
}, false);
|
||||
return newList;
|
||||
}, false);
|
||||
|
||||
const newPayload: any = { ...payload };
|
||||
|
||||
if (newPayload.sort_order && payload.sort_order)
|
||||
newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||
|
||||
cyclesService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user);
|
||||
};
|
||||
@ -63,9 +62,7 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
|
||||
const blockFormat = (blocks: ICycle[]) =>
|
||||
blocks && blocks.length > 0
|
||||
? blocks
|
||||
.filter(
|
||||
(b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)
|
||||
)
|
||||
.filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date))
|
||||
.map((block) => ({
|
||||
data: block,
|
||||
id: block.id,
|
||||
|
37
web/constants/cycle.ts
Normal file
37
web/constants/cycle.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export const CYCLE_TAB_LIST = [
|
||||
{
|
||||
key: "all",
|
||||
name: "All",
|
||||
},
|
||||
{
|
||||
key: "active",
|
||||
name: "Active",
|
||||
},
|
||||
{
|
||||
key: "upcoming",
|
||||
name: "Upcoming",
|
||||
},
|
||||
{
|
||||
key: "completed",
|
||||
name: "Completed",
|
||||
},
|
||||
{
|
||||
key: "draft",
|
||||
name: "Drafts",
|
||||
},
|
||||
];
|
||||
|
||||
export const CYCLE_VIEWS = [
|
||||
{
|
||||
key: "list",
|
||||
icon: "list",
|
||||
},
|
||||
{
|
||||
key: "board",
|
||||
icon: "dataset",
|
||||
},
|
||||
{
|
||||
key: "gantt",
|
||||
icon: "view_timeline",
|
||||
},
|
||||
];
|
@ -1,24 +1,14 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import {
|
||||
ActiveCycleDetails,
|
||||
AllCyclesList,
|
||||
CompletedCyclesList,
|
||||
CreateUpdateCycleModal,
|
||||
DraftCyclesList,
|
||||
UpcomingCyclesList,
|
||||
} from "components/cycles";
|
||||
import { CyclesList, ActiveCycleDetails, CreateUpdateCycleModal } from "components/cycles";
|
||||
// ui
|
||||
import { EmptyState, Icon, PrimaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
@ -31,54 +21,41 @@ import { SelectCycleType } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// constants
|
||||
import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle";
|
||||
|
||||
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
|
||||
type ICycleAPIFilter = "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
|
||||
|
||||
const cycleViews = [
|
||||
{
|
||||
key: "list",
|
||||
icon: "list",
|
||||
},
|
||||
{
|
||||
key: "board",
|
||||
icon: "dataset",
|
||||
},
|
||||
{
|
||||
key: "gantt",
|
||||
icon: "view_timeline",
|
||||
},
|
||||
];
|
||||
|
||||
const ProjectCycles: NextPage = () => {
|
||||
const ProjectCyclesPage: NextPage = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
|
||||
// states
|
||||
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All");
|
||||
const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list");
|
||||
|
||||
const currentTabValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "All":
|
||||
return 0;
|
||||
case "Active":
|
||||
return 1;
|
||||
case "Upcoming":
|
||||
return 2;
|
||||
case "Completed":
|
||||
return 3;
|
||||
case "Drafts":
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
// local storage
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "all");
|
||||
const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycle_view", "list");
|
||||
// hooks
|
||||
const { user } = useUserAuth();
|
||||
const { projectDetails } = useProjectDetails();
|
||||
// api call fetch project details
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
||||
workspaceSlug && projectId
|
||||
? () => {
|
||||
projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
: null
|
||||
);
|
||||
|
||||
/**
|
||||
* Clearing form data after closing the modal
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (createUpdateCycleModal) return;
|
||||
|
||||
@ -88,6 +65,12 @@ const ProjectCycles: NextPage = () => {
|
||||
}, 500);
|
||||
}, [createUpdateCycleModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cycleTab === "draft" && cyclesView === "gantt") {
|
||||
setCyclesView("list");
|
||||
}
|
||||
}, [cycleTab, cyclesView, setCyclesView]);
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
@ -137,60 +120,42 @@ const ProjectCycles: NextPage = () => {
|
||||
<Tab.Group
|
||||
as="div"
|
||||
className="h-full flex flex-col overflow-hidden"
|
||||
defaultIndex={currentTabValue(cycleTab)}
|
||||
selectedIndex={currentTabValue(cycleTab)}
|
||||
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key === cycleTab)}
|
||||
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key === cycleTab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setCycleTab("All");
|
||||
case 1:
|
||||
return setCycleTab("Active");
|
||||
case 2:
|
||||
return setCycleTab("Upcoming");
|
||||
case 3:
|
||||
return setCycleTab("Completed");
|
||||
case 4:
|
||||
return setCycleTab("Drafts");
|
||||
default:
|
||||
return setCycleTab("All");
|
||||
try {
|
||||
setCycleTab(CYCLE_TAB_LIST[i].key);
|
||||
} catch (e) {
|
||||
setCycleTab(CYCLE_TAB_LIST[0].key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-between border-b border-custom-border-300 px-4 sm:px-5 pb-4 sm:pb-0">
|
||||
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||
{tabsList.map((tab, index) => {
|
||||
if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts"))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
`border-b-2 p-4 text-sm font-medium outline-none ${
|
||||
selected
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
{CYCLE_TAB_LIST.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`border-b-2 p-4 text-sm font-medium outline-none ${
|
||||
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<div className="justify-end sm:justify-start flex items-center gap-x-1">
|
||||
{cycleViews.map((view) => {
|
||||
if (cycleTab === "Active") return null;
|
||||
if (view.key === "gantt" && cycleTab === "Drafts") return null;
|
||||
{CYCLE_VIEWS.map((view) => {
|
||||
if (cycleTab === "active") return null;
|
||||
if (view.key === "gantt" && cycleTab === "draft") return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={view.key}
|
||||
type="button"
|
||||
className={`grid h-8 w-8 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-80 ${
|
||||
cyclesView === view.key
|
||||
? "bg-custom-background-80 text-custom-text-100"
|
||||
: "text-custom-text-200"
|
||||
cyclesView === view.key ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
onClick={() => setCyclesView(view.key)}
|
||||
>
|
||||
@ -202,29 +167,25 @@ const ProjectCycles: NextPage = () => {
|
||||
</div>
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
<AllCyclesList viewType={cyclesView} />
|
||||
{cycleTab && <CyclesList filter={cycleTab as ICycleAPIFilter} />}
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
|
||||
<ActiveCycleDetails />
|
||||
</Tab.Panel>
|
||||
)}
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
<UpcomingCyclesList viewType={cyclesView} />
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
|
||||
<ActiveCycleDetails />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
<CompletedCyclesList viewType={cyclesView} />
|
||||
{cycleTab && <CyclesList filter={cycleTab as ICycleAPIFilter} />}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
{cycleTab && <CyclesList filter={cycleTab as ICycleAPIFilter} />}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
{cycleTab && <CyclesList filter={cycleTab as ICycleAPIFilter} />}
|
||||
</Tab.Panel>
|
||||
{cyclesView !== "gantt_chart" && (
|
||||
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||
<DraftCyclesList viewType={cyclesView} />
|
||||
</Tab.Panel>
|
||||
)}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
)}
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default ProjectCycles;
|
||||
export default ProjectCyclesPage;
|
||||
|
@ -18,6 +18,12 @@ export interface ICycleStore {
|
||||
cycle_details: {
|
||||
[cycle_id: string]: ICycle;
|
||||
};
|
||||
|
||||
fetchCycles: (
|
||||
workspaceSlug: string,
|
||||
projectSlug: string,
|
||||
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
class CycleStore implements ICycleStore {
|
||||
@ -47,8 +53,9 @@ class CycleStore implements ICycleStore {
|
||||
cycles: observable.ref,
|
||||
|
||||
// computed
|
||||
|
||||
projectCycles: computed,
|
||||
// actions
|
||||
fetchCycles: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
@ -64,12 +71,16 @@ class CycleStore implements ICycleStore {
|
||||
}
|
||||
|
||||
// actions
|
||||
fetchCycles = async (workspaceSlug: string, projectSlug: string) => {
|
||||
fetchCycles = async (
|
||||
workspaceSlug: string,
|
||||
projectSlug: string,
|
||||
params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"
|
||||
) => {
|
||||
try {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
|
||||
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectSlug, "all");
|
||||
const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectSlug, params);
|
||||
|
||||
runInAction(() => {
|
||||
this.cycles = {
|
||||
|
Loading…
Reference in New Issue
Block a user