feat: issues filter using views (#448)

* fix: made basic UI for views, binded services and logic for views

* feat: views list, delete view, and conditionally updating filters or my view props
This commit is contained in:
Dakshesh Jain 2023-03-16 14:07:19 +05:30 committed by GitHub
parent 96ad751e11
commit ef0e326ca0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 509 additions and 127 deletions

View File

@ -97,26 +97,57 @@ export const IssuesFilterView: React.FC = () => {
</button>
</div>
<CustomMenu
customButton={
<button
type="button"
className="group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none"
>
label={
<span className="flex items-center gap-2 rounded-md py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
Filters
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</button>
</span>
}
optionsPosition="right"
>
<CustomMenu.MenuItem
onClick={() =>
setFilters({
assignees: ["72d6ad43-41ff-4907-9980-2f5ee8745ad3"],
})
}
>
Member- Aaryan
</CustomMenu.MenuItem>
<h4 className="px-1 py-2 font-medium">Status</h4>
{statesList?.map((state) => (
<CustomMenu.MenuItem
onClick={() => {
const filterStates = filters?.state ?? [];
const newFilterState = filterStates.includes(state.id)
? filterStates.filter((id) => id !== state.id)
: [...filterStates, state.id];
setFilters({ ...filters, state: newFilterState });
}}
>
<>{state.name}</>
</CustomMenu.MenuItem>
))}
<h4 className="px-1 py-2 font-medium">Members</h4>
{members?.map((member) => (
<CustomMenu.MenuItem onClick={() => {}}>
<>
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name + " " + member.member.last_name
: member.member.email}
</>
</CustomMenu.MenuItem>
))}
<h4 className="px-1 py-2 font-medium">Labels</h4>
{issueLabels?.map((label) => (
<CustomMenu.MenuItem onClick={() => {}}>
<>{label.name}</>
</CustomMenu.MenuItem>
))}
<h4 className="px-1 py-2 font-medium">Priority</h4>
{PRIORITIES?.map((priority) => (
<CustomMenu.MenuItem
onClick={() => {
if (priority === null) return;
const filterPriorities = filters?.priority ?? [];
const newFilterPriority = filterPriorities.includes(priority)
? filterPriorities.filter((id) => id !== priority)
: [...filterPriorities, priority];
setFilters({ ...filters, priority: newFilterPriority });
}}
>
<span className="capitalize">{priority ?? "None"}</span>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<Popover className="relative">
{({ open }) => (

View File

@ -17,12 +17,20 @@ import useIssuesView from "hooks/use-issues-view";
import { AllLists, AllBoards } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CreateUpdateViewModal } from "components/views";
// icons
import { PlusIcon, RectangleStackIcon, TrashIcon } from "@heroicons/react/24/outline";
import { PlusIcon, RectangleStackIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { CycleIssueResponse, IIssue, ModuleIssueResponse, UserAuth } from "types";
import {
CycleIssueResponse,
IIssue,
IIssueFilterOptions,
IView,
ModuleIssueResponse,
UserAuth,
} from "types";
// fetch-keys
import {
CYCLE_ISSUES,
@ -44,6 +52,7 @@ type Props = {
export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModal, userAuth }) => {
// create issue modal
const [createIssueModal, setCreateIssueModal] = useState(false);
const [createViewModal, setCreateViewModal] = useState<any>(null);
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
>(undefined);
@ -360,6 +369,11 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
return (
<>
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
preLoadedData={createViewModal}
/>
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
@ -378,24 +392,95 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
isOpen={deleteIssueModal}
data={issueToDelete}
/>
<div className="flex items-center gap-2">
{Object.keys(filters).map((key) => {
if (filters[key as keyof typeof filters] !== null)
return (
<button
key={key}
type="button"
className="rounded bg-black p-2 text-xs text-white"
onClick={() =>
setFilters({
[key]: null,
})
}
>
Remove {key} filter
</button>
);
})}
<div className="mb-3 flex items-center justify-between gap-2">
<div className="flex gap-x-3">
{Object.keys(filters).map((key) => {
if (filters[key as keyof typeof filters] !== null)
return (
<div key={key} className="flex gap-x-2 text-sm">
<p>
Filter for <span className="font-medium">{key}</span>:{" "}
</p>
{filters[key as keyof IIssueFilterOptions] === null ||
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
<p className="font-medium">None</p>
) : (
Array.isArray(filters[key as keyof IIssueFilterOptions]) && (
<p className="space-x-2 font-medium">
{key === "state"
? (filters[key as keyof IIssueFilterOptions] as any)?.map(
(stateId: any) => {
const state = states?.find((s) => s.id === stateId);
return (
<p
key={state?.id}
className="inline-flex items-center gap-x-1 rounded-full bg-gray-500 px-2 py-0.5 text-xs font-medium text-white"
>
<span>{state?.name ?? "Loading..."}</span>
<span
className="cursor-pointer"
onClick={() => {
setFilters({
...filters,
[key]: (
filters[key as keyof IIssueFilterOptions] as any
)?.filter((s: any) => s !== stateId),
});
}}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
);
}
)
: key === "priority"
? (filters[key as keyof IIssueFilterOptions] as any)?.map(
(priority: any) => (
<p
key={priority}
className="inline-flex items-center gap-x-1 rounded-full bg-gray-500 px-2 py-0.5 text-xs font-medium capitalize text-white"
>
<span>{priority}</span>
<span
className="cursor-pointer"
onClick={() => {
setFilters({
...filters,
[key]: (
filters[key as keyof IIssueFilterOptions] as any
)?.filter((p: any) => p !== priority),
});
}}
>
<XMarkIcon className="h-3 w-3" />
</span>
</p>
)
)
: (filters[key as keyof IIssueFilterOptions] as any)?.join(", ")}
</p>
)
)}
</div>
);
})}
</div>
<div>
<button
type="button"
onClick={() =>
setCreateViewModal({
query: filters,
})
}
className="flex items-center gap-x-0.5 text-sm"
>
<PlusIcon className="h-3 w-3" />
<span>Save view</span>
</button>
</div>
</div>
<DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox">

View File

@ -41,4 +41,5 @@ export * from "./assignment-clipboard-icon";
export * from "./tick-mark-icon";
export * from "./contrast-icon";
export * from "./people-group-icon";
export * from "./cmd-icon";
export * from "./cmd-icon";
export * from "./view-list-icon";

View File

@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const ViewListIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "#858E96",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.25 13.125V4.875C2.25 4.5625 2.35938 4.29688 2.57812 4.07812C2.79688 3.85938 3.0625 3.75 3.375 3.75H14.625C14.9375 3.75 15.2031 3.85938 15.4219 4.07812C15.6406 4.29688 15.75 4.5625 15.75 4.875V13.125C15.75 13.4375 15.6406 13.7031 15.4219 13.9219C15.2031 14.1406 14.9375 14.25 14.625 14.25H3.375C3.0625 14.25 2.79688 14.1406 2.57812 13.9219C2.35938 13.7031 2.25 13.4375 2.25 13.125ZM3.375 6.88125H5.3625V4.875H3.375V6.88125ZM6.4875 6.88125H14.625V4.875H6.4875V6.88125ZM6.4875 9.99375H14.625V8.00625H6.4875V9.99375ZM6.4875 13.125H14.625V11.1187H6.4875V13.125ZM3.375 13.125H5.3625V11.1187H3.375V13.125ZM3.375 9.99375H5.3625V8.00625H3.375V9.99375Z"
fill={color}
/>
</svg>
);

View File

@ -7,7 +7,13 @@ import { Disclosure, Transition } from "@headlessui/react";
import { CustomMenu } from "components/ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { ContrastIcon, LayerDiagonalIcon, PeopleGroupIcon, SettingIcon } from "components/icons";
import {
ContrastIcon,
LayerDiagonalIcon,
PeopleGroupIcon,
SettingIcon,
ViewListIcon,
} from "components/icons";
// helpers
import { truncateText } from "helpers/string.helper";
// types
@ -38,6 +44,11 @@ const navigation = (workspaceSlug: string, projectId: string) => [
href: `/${workspaceSlug}/projects/${projectId}/modules`,
icon: PeopleGroupIcon,
},
{
name: "Views",
href: `/${workspaceSlug}/projects/${projectId}/views`,
icon: ViewListIcon,
},
{
name: "Settings",
href: `/${workspaceSlug}/projects/${projectId}/settings`,

View File

@ -21,11 +21,12 @@ import { VIEWS_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
data?: IView;
data: IView | null;
onClose: () => void;
onSuccess?: () => void;
};
export const DeleteViewModal: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, onClose, onSuccess }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter();
@ -36,19 +37,23 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
const cancelButtonRef = useRef(null);
const handleClose = () => {
setIsOpen(false);
setIsDeleteLoading(false);
onClose();
};
const handleDeletion = async () => {
setIsDeleteLoading(true);
if (!workspaceSlug || !data) return;
if (!workspaceSlug || !data || !projectId) return;
await viewsService
.deleteView(projectId as string, data.id)
.deleteView(workspaceSlug as string, projectId as string, data.id)
.then(() => {
mutate(VIEWS_LIST(projectId as string));
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
views?.filter((view) => view.id !== data.id)
);
if (onSuccess) onSuccess();
handleClose();
setToastAlert({

View File

@ -12,6 +12,7 @@ type Props = {
handleClose: () => void;
status: boolean;
data?: IView;
preLoadedData?: Partial<IView> | null;
};
const defaultValues: Partial<IView> = {
@ -19,7 +20,13 @@ const defaultValues: Partial<IView> = {
description: "",
};
export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
export const ViewForm: React.FC<Props> = ({
handleFormSubmit,
handleClose,
status,
data,
preLoadedData,
}) => {
const {
register,
formState: { errors, isSubmitting },
@ -44,6 +51,13 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
});
}, [data, reset]);
useEffect(() => {
reset({
...defaultValues,
...preLoadedData,
});
}, [preLoadedData, reset]);
return (
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
<div className="space-y-5">

View File

@ -4,8 +4,6 @@ import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
@ -23,14 +21,15 @@ type Props = {
isOpen: boolean;
handleClose: () => void;
data?: IView;
preLoadedData?: Partial<IView> | null;
};
const defaultValues: Partial<IView> = {
name: "",
description: "",
};
export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, data }) => {
export const CreateUpdateViewModal: React.FC<Props> = ({
isOpen,
handleClose,
data,
preLoadedData,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -38,14 +37,13 @@ export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, da
const onClose = () => {
handleClose();
reset(defaultValues);
};
const { reset } = useForm<IView>({
defaultValues,
});
const createView = async (payload: IView) => {
payload = {
...payload,
query_data: payload.query,
};
await viewsService
.createView(workspaceSlug as string, projectId as string, payload)
.then(() => {
@ -137,6 +135,7 @@ export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, da
handleClose={handleClose}
status={data ? true : false}
data={data}
preLoadedData={preLoadedData}
/>
</Dialog.Panel>
</Transition.Child>

View File

@ -8,6 +8,7 @@ import useSWR, { mutate } from "swr";
import ToastAlert from "components/toast-alert";
// services
import projectService from "services/project.service";
import viewsService from "services/views.service";
// types
import { IIssueFilterOptions, IProjectMember, NestedKeyOf } from "types";
// fetch-keys
@ -16,6 +17,8 @@ import {
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
USER_PROJECT_VIEW,
VIEW_DETAILS,
VIEW_ISSUES,
} from "constants/fetch-keys";
export const issueViewContext = createContext<ContextType>({} as ContextType);
@ -62,8 +65,10 @@ export const initialState: StateType = {
orderBy: "created_at",
filters: {
type: null,
priority: null,
assignees: null,
labels: null,
state: null,
issue__assignees__id: null,
issue__labels__id: null,
},
@ -150,6 +155,17 @@ const saveDataToServer = async (workspaceSlug: string, projectID: string, state:
});
};
const sendFilterDataToServer = async (
workspaceSlug: string,
projectID: string,
viewId: string,
state: any
) => {
await viewsService.patchView(workspaceSlug, projectID, viewId, {
...state,
});
};
const setNewDefault = async (workspaceSlug: string, projectId: string, state: any) => {
mutate<IProjectMember>(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
@ -174,7 +190,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
@ -183,6 +199,18 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
: null
);
const { data: viewDetails, mutate: mutateViewDetails } = useSWR(
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
workspaceSlug && projectId && viewId
? () =>
viewsService.getViewDetails(
workspaceSlug as string,
projectId as string,
viewId as string
)
: null
);
const setIssueViewToKanban = useCallback(() => {
dispatch({
type: "SET_ISSUE_VIEW",
@ -335,15 +363,33 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
};
}, false);
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
filters: {
...state.filters,
...property,
},
});
if (viewId) {
mutateViewDetails((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
query_data: {
...state.filters,
...property,
},
};
}, false);
sendFilterDataToServer(workspaceSlug as string, projectId as string, viewId as string, {
query_data: {
...state.filters,
...property,
},
});
} else
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
filters: {
...state.filters,
...property,
},
});
},
[projectId, workspaceSlug, state, mutateMyViewProps]
[projectId, workspaceSlug, state, mutateMyViewProps, viewId, mutateViewDetails]
);
const setNewDefaultView = useCallback(() => {
@ -368,9 +414,15 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
useEffect(() => {
dispatch({
type: "REHYDRATE_THEME",
payload: myViewProps?.view_props,
payload: {
...myViewProps?.view_props,
filters: {
...myViewProps?.view_props?.filters,
...viewDetails?.query_data,
} as any,
},
});
}, [myViewProps]);
}, [myViewProps, viewDetails]);
useEffect(() => {
// TODO: think of a better way to do this
@ -380,11 +432,14 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
} else if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string), {}, false);
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string));
} else if (viewId) {
mutate(VIEW_ISSUES(viewId as string), {}, false);
mutate(VIEW_ISSUES(viewId as string));
} else {
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string), {}, false);
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
}
}, [state, projectId, cycleId, moduleId]);
}, [state, projectId, cycleId, moduleId, viewId]);
return (
<issueViewContext.Provider

View File

@ -15,10 +15,12 @@ import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
VIEW_ISSUES,
} from "constants/fetch-keys";
// types
import type { IIssue } from "types";
import viewsService from "services/views.service";
const useIssuesView = () => {
const {
@ -36,18 +38,22 @@ const useIssuesView = () => {
} = useContext(issueViewContext);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const params: any = {
order_by: orderBy,
group_by: groupByProperty,
assignees: filters.assignees ? filters.assignees.join(",") : undefined,
type: filters.type ? filters.type : undefined,
labels: filters.labels ? filters.labels.join(",") : undefined,
issue__assignees__id: filters.issue__assignees__id
? filters.issue__assignees__id.join(",")
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
state: filters?.state ? filters?.state.join(",") : undefined,
priority: filters?.priority ? filters?.priority.join(",") : undefined,
type: filters?.type ? filters?.type : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined,
issue__assignees__id: filters?.issue__assignees__id
? filters?.issue__assignees__id.join(",")
: undefined,
issue__labels__id: filters?.issue__labels__id
? filters?.issue__labels__id.join(",")
: undefined,
issue__labels__id: filters.issue__labels__id ? filters.issue__labels__id.join(",") : undefined,
};
const { data: projectIssues } = useSWR(
@ -60,6 +66,14 @@ const useIssuesView = () => {
: null
);
const { data: viewIssues } = useSWR(
workspaceSlug && projectId && viewId ? VIEW_ISSUES(viewId as string) : null,
workspaceSlug && projectId && viewId
? () =>
viewsService.getViewIssues(workspaceSlug as string, projectId as string, viewId as string)
: null
);
const { data: cycleIssues } = useSWR(
workspaceSlug && projectId && cycleId && params
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string)
@ -95,11 +109,11 @@ const useIssuesView = () => {
[key: string]: IIssue[];
}
| undefined = useMemo(() => {
const issuesToGroup = cycleIssues ?? moduleIssues ?? projectIssues;
const issuesToGroup = viewIssues ?? cycleIssues ?? moduleIssues ?? projectIssues;
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
else return issuesToGroup;
}, [projectIssues, cycleIssues, moduleIssues]);
}, [projectIssues, cycleIssues, moduleIssues, viewIssues]);
return {
groupedByIssues,

View File

@ -10,6 +10,8 @@ import projectService from "services/project.service";
import viewsService from "services/views.service";
// layouts
import AppLayout from "layouts/app-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { CreateUpdateViewModal, DeleteViewModal } from "components/views";
// ui
@ -18,6 +20,9 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { UserAuth } from "types";
// fetch-keys
import { PROJECT_DETAILS, VIEW_DETAILS, VIEW_ISSUES } from "constants/fetch-keys";
import { IssuesFilterView, IssuesView } from "components/core";
import { HeaderButton } from "components/ui";
import { PlusIcon } from "@heroicons/react/24/outline";
const SingleView: React.FC<UserAuth> = (props) => {
const router = useRouter();
@ -42,27 +47,36 @@ const SingleView: React.FC<UserAuth> = (props) => {
: null
);
const { data: viewIssues } = useSWR(
workspaceSlug && projectId && viewId ? VIEW_ISSUES(viewId as string) : null,
workspaceSlug && projectId && viewId
? () =>
viewsService.getViewIssues(workspaceSlug as string, projectId as string, viewId as string)
: null
);
return (
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"} Views`}
link={`/${workspaceSlug}/projects/${activeProject?.id}/cycles`}
/>
</Breadcrumbs>
}
>
Content here
</AppLayout>
<IssueViewContextProvider>
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"} Views`}
link={`/${workspaceSlug}/projects/${activeProject?.id}/cycles`}
/>
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<IssuesFilterView />
<HeaderButton
Icon={PlusIcon}
label="Add Issue"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
/>
</div>
}
>
<IssuesView userAuth={props} />
</AppLayout>
</IssueViewContextProvider>
);
};

View File

@ -0,0 +1,124 @@
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// lib
import { requiredAuth } from "lib/auth";
// services
import viewsService from "services/views.service";
import projectService from "services/project.service";
// layouts
import AppLayout from "layouts/app-layout";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { TrashIcon } from "@heroicons/react/20/solid";
// fetching keys
import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys";
// components
import { CustomMenu, Spinner } from "components/ui";
import { DeleteViewModal } from "components/views";
// types
import { IView } from "types";
import type { NextPage, GetServerSidePropsContext } from "next";
const ProjectViews: NextPage = () => {
const [selectedView, setSelectedView] = useState<IView | null>(null);
const {
query: { workspaceSlug, projectId },
} = useRouter();
const { data: activeProject } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
const { data: views } = useSWR(
workspaceSlug && projectId ? VIEWS_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => viewsService.getViews(workspaceSlug as string, projectId as string)
: null
);
return (
<AppLayout
meta={{
title: "Plane - Views",
}}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
</Breadcrumbs>
}
>
<DeleteViewModal
isOpen={!!selectedView}
data={selectedView}
onClose={() => setSelectedView(null)}
onSuccess={() => setSelectedView(null)}
/>
<div className="rounded-md border border-gray-400">
{views ? (
views.map((view) => (
<div
className="flex items-center justify-between border-b border-gray-400 p-4 last:border-b-0"
key={view.id}
>
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
<a>{view.name}</a>
</Link>
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem
onClick={() => {
setSelectedView(view);
}}
>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-4 w-4" />
<span>Delete</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
))
) : (
<div className="flex justify-center pt-20">
<Spinner />
</div>
)}
</div>
</AppLayout>
);
};
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const user = await requiredAuth(ctx.req?.headers.cookie);
const redirectAfterSignIn = ctx.resolvedUrl;
if (!user) {
return {
redirect: {
destination: `/signin?next=${redirectAfterSignIn}`,
permanent: false,
},
};
}
return {
props: {
user,
},
};
};
export default ProjectViews;

View File

@ -47,8 +47,8 @@ class ViewServices extends APIService {
});
}
async deleteView(projectId: string, viewId: string): Promise<any> {
return this.delete(`/api/projects/${projectId}/views/${viewId}/`)
async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;

View File

@ -210,9 +210,11 @@ export interface IIssueActivity {
export interface IIssueFilterOptions {
type: "active" | "backlog" | null;
assignees: string[] | null;
state: string[] | null;
labels: string[] | null;
issue__assignees__id: string[] | null;
issue__labels__id: string[] | null;
priority: string[] | null;
}
export interface IIssueViewOptions {

View File

@ -7,29 +7,32 @@ export interface IView {
updated_by: string;
name: string;
description: string;
query: {
state: string[] | null;
parent: string[] | null;
labels: string[] | null;
assignees: string[] | null;
created_by: string[] | null;
name: string | null;
created_at: [
{
datetime: string;
timeline: "before";
},
{
datetime: string;
timeline: "after";
}
];
updated_at: string[] | null;
start_date: string[] | null;
target_date: string[] | null;
completed_at: string[] | null;
type: string;
};
query: IQuery;
query_data: IQuery;
project: string;
workspace: string;
}
export interface IQuery {
state: string[] | null;
parent: string[] | null;
labels: string[] | null;
assignees: string[] | null;
created_by: string[] | null;
name: string | null;
created_at: [
{
datetime: string;
timeline: "before";
},
{
datetime: string;
timeline: "after";
}
];
updated_at: string[] | null;
start_date: string[] | null;
target_date: string[] | null;
completed_at: string[] | null;
type: string;
}