forked from github/plane
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:
parent
96ad751e11
commit
ef0e326ca0
@ -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 }) => (
|
||||
|
@ -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">
|
||||
|
@ -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";
|
||||
|
24
apps/app/components/icons/view-list-icon.tsx
Normal file
24
apps/app/components/icons/view-list-icon.tsx
Normal 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>
|
||||
);
|
@ -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`,
|
||||
|
@ -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({
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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;
|
||||
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -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 {
|
||||
|
49
apps/app/types/views.d.ts
vendored
49
apps/app/types/views.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user