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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
label={
|
||||||
<button
|
<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">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Filters
|
Filters
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
</span>
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
optionsPosition="right"
|
|
||||||
>
|
>
|
||||||
|
<h4 className="px-1 py-2 font-medium">Status</h4>
|
||||||
|
{statesList?.map((state) => (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
setFilters({
|
const filterStates = filters?.state ?? [];
|
||||||
assignees: ["72d6ad43-41ff-4907-9980-2f5ee8745ad3"],
|
const newFilterState = filterStates.includes(state.id)
|
||||||
})
|
? filterStates.filter((id) => id !== state.id)
|
||||||
}
|
: [...filterStates, state.id];
|
||||||
|
setFilters({ ...filters, state: newFilterState });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Member- Aaryan
|
<>{state.name}</>
|
||||||
</CustomMenu.MenuItem>
|
</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>
|
</CustomMenu>
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
|
@ -17,12 +17,20 @@ import useIssuesView from "hooks/use-issues-view";
|
|||||||
import { AllLists, AllBoards } from "components/core";
|
import { AllLists, AllBoards } from "components/core";
|
||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, RectangleStackIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon, RectangleStackIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
import { CycleIssueResponse, IIssue, ModuleIssueResponse, UserAuth } from "types";
|
import {
|
||||||
|
CycleIssueResponse,
|
||||||
|
IIssue,
|
||||||
|
IIssueFilterOptions,
|
||||||
|
IView,
|
||||||
|
ModuleIssueResponse,
|
||||||
|
UserAuth,
|
||||||
|
} from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
CYCLE_ISSUES,
|
CYCLE_ISSUES,
|
||||||
@ -44,6 +52,7 @@ type Props = {
|
|||||||
export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModal, userAuth }) => {
|
export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModal, userAuth }) => {
|
||||||
// create issue modal
|
// create issue modal
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||||
|
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
||||||
const [preloadedData, setPreloadedData] = useState<
|
const [preloadedData, setPreloadedData] = useState<
|
||||||
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@ -360,6 +369,11 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<CreateUpdateViewModal
|
||||||
|
isOpen={createViewModal !== null}
|
||||||
|
handleClose={() => setCreateViewModal(null)}
|
||||||
|
preLoadedData={createViewModal}
|
||||||
|
/>
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||||
handleClose={() => setCreateIssueModal(false)}
|
handleClose={() => setCreateIssueModal(false)}
|
||||||
@ -378,25 +392,96 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
|
|||||||
isOpen={deleteIssueModal}
|
isOpen={deleteIssueModal}
|
||||||
data={issueToDelete}
|
data={issueToDelete}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="mb-3 flex items-center justify-between gap-2">
|
||||||
|
<div className="flex gap-x-3">
|
||||||
{Object.keys(filters).map((key) => {
|
{Object.keys(filters).map((key) => {
|
||||||
if (filters[key as keyof typeof filters] !== null)
|
if (filters[key as keyof typeof filters] !== null)
|
||||||
return (
|
return (
|
||||||
<button
|
<div key={key} className="flex gap-x-2 text-sm">
|
||||||
key={key}
|
<p>
|
||||||
type="button"
|
Filter for <span className="font-medium">{key}</span>:{" "}
|
||||||
className="rounded bg-black p-2 text-xs text-white"
|
</p>
|
||||||
onClick={() =>
|
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||||
setFilters({
|
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
|
||||||
[key]: null,
|
<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"
|
||||||
>
|
>
|
||||||
Remove {key} filter
|
<span>{state?.name ?? "Loading..."}</span>
|
||||||
</button>
|
<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>
|
||||||
|
|
||||||
|
<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}>
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
<StrictModeDroppable droppableId="trashBox">
|
<StrictModeDroppable droppableId="trashBox">
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
|
@ -42,3 +42,4 @@ export * from "./tick-mark-icon";
|
|||||||
export * from "./contrast-icon";
|
export * from "./contrast-icon";
|
||||||
export * from "./people-group-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";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
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
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -38,6 +44,11 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
|||||||
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
||||||
icon: PeopleGroupIcon,
|
icon: PeopleGroupIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Views",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/views`,
|
||||||
|
icon: ViewListIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
||||||
|
@ -21,11 +21,12 @@ import { VIEWS_LIST } from "constants/fetch-keys";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
data: IView | null;
|
||||||
data?: IView;
|
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 [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -36,19 +37,23 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
|
|||||||
const cancelButtonRef = useRef(null);
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeletion = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
if (!workspaceSlug || !data) return;
|
if (!workspaceSlug || !data || !projectId) return;
|
||||||
await viewsService
|
await viewsService
|
||||||
.deleteView(projectId as string, data.id)
|
.deleteView(workspaceSlug as string, projectId as string, data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(VIEWS_LIST(projectId as string));
|
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
views?.filter((view) => view.id !== data.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -12,6 +12,7 @@ type Props = {
|
|||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
data?: IView;
|
data?: IView;
|
||||||
|
preLoadedData?: Partial<IView> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IView> = {
|
const defaultValues: Partial<IView> = {
|
||||||
@ -19,7 +20,13 @@ const defaultValues: Partial<IView> = {
|
|||||||
description: "",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
|
export const ViewForm: React.FC<Props> = ({
|
||||||
|
handleFormSubmit,
|
||||||
|
handleClose,
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
preLoadedData,
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
@ -44,6 +51,13 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
|
|||||||
});
|
});
|
||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({
|
||||||
|
...defaultValues,
|
||||||
|
...preLoadedData,
|
||||||
|
});
|
||||||
|
}, [preLoadedData, reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
@ -4,8 +4,6 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
@ -23,14 +21,15 @@ type Props = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
data?: IView;
|
data?: IView;
|
||||||
|
preLoadedData?: Partial<IView> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IView> = {
|
export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||||
name: "",
|
isOpen,
|
||||||
description: "",
|
handleClose,
|
||||||
};
|
data,
|
||||||
|
preLoadedData,
|
||||||
export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, data }) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -38,14 +37,13 @@ export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, da
|
|||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
reset(defaultValues);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { reset } = useForm<IView>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createView = async (payload: IView) => {
|
const createView = async (payload: IView) => {
|
||||||
|
payload = {
|
||||||
|
...payload,
|
||||||
|
query_data: payload.query,
|
||||||
|
};
|
||||||
await viewsService
|
await viewsService
|
||||||
.createView(workspaceSlug as string, projectId as string, payload)
|
.createView(workspaceSlug as string, projectId as string, payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -137,6 +135,7 @@ export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, da
|
|||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
status={data ? true : false}
|
status={data ? true : false}
|
||||||
data={data}
|
data={data}
|
||||||
|
preLoadedData={preLoadedData}
|
||||||
/>
|
/>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
@ -8,6 +8,7 @@ import useSWR, { mutate } from "swr";
|
|||||||
import ToastAlert from "components/toast-alert";
|
import ToastAlert from "components/toast-alert";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
|
import viewsService from "services/views.service";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IProjectMember, NestedKeyOf } from "types";
|
import { IIssueFilterOptions, IProjectMember, NestedKeyOf } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -16,6 +17,8 @@ import {
|
|||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
USER_PROJECT_VIEW,
|
USER_PROJECT_VIEW,
|
||||||
|
VIEW_DETAILS,
|
||||||
|
VIEW_ISSUES,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
||||||
@ -62,8 +65,10 @@ export const initialState: StateType = {
|
|||||||
orderBy: "created_at",
|
orderBy: "created_at",
|
||||||
filters: {
|
filters: {
|
||||||
type: null,
|
type: null,
|
||||||
|
priority: null,
|
||||||
assignees: null,
|
assignees: null,
|
||||||
labels: null,
|
labels: null,
|
||||||
|
state: null,
|
||||||
issue__assignees__id: null,
|
issue__assignees__id: null,
|
||||||
issue__labels__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) => {
|
const setNewDefault = async (workspaceSlug: string, projectId: string, state: any) => {
|
||||||
mutate<IProjectMember>(
|
mutate<IProjectMember>(
|
||||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
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 [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
|
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
|
||||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
||||||
@ -183,6 +199,18 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
: null
|
: 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(() => {
|
const setIssueViewToKanban = useCallback(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_ISSUE_VIEW",
|
type: "SET_ISSUE_VIEW",
|
||||||
@ -335,6 +363,24 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
};
|
};
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
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, {
|
saveDataToServer(workspaceSlug as string, projectId as string, {
|
||||||
...state,
|
...state,
|
||||||
filters: {
|
filters: {
|
||||||
@ -343,7 +389,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[projectId, workspaceSlug, state, mutateMyViewProps]
|
[projectId, workspaceSlug, state, mutateMyViewProps, viewId, mutateViewDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setNewDefaultView = useCallback(() => {
|
const setNewDefaultView = useCallback(() => {
|
||||||
@ -368,9 +414,15 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "REHYDRATE_THEME",
|
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(() => {
|
useEffect(() => {
|
||||||
// TODO: think of a better way to do this
|
// TODO: think of a better way to do this
|
||||||
@ -380,11 +432,14 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
} else if (moduleId) {
|
} else if (moduleId) {
|
||||||
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string), {}, false);
|
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string), {}, false);
|
||||||
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string));
|
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 {
|
} else {
|
||||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string), {}, false);
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string), {}, false);
|
||||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string));
|
||||||
}
|
}
|
||||||
}, [state, projectId, cycleId, moduleId]);
|
}, [state, projectId, cycleId, moduleId, viewId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<issueViewContext.Provider
|
<issueViewContext.Provider
|
||||||
|
@ -15,10 +15,12 @@ import {
|
|||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
VIEW_ISSUES,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
|
import viewsService from "services/views.service";
|
||||||
|
|
||||||
const useIssuesView = () => {
|
const useIssuesView = () => {
|
||||||
const {
|
const {
|
||||||
@ -36,18 +38,22 @@ const useIssuesView = () => {
|
|||||||
} = useContext(issueViewContext);
|
} = useContext(issueViewContext);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const params: any = {
|
const params: any = {
|
||||||
order_by: orderBy,
|
order_by: orderBy,
|
||||||
group_by: groupByProperty,
|
group_by: groupByProperty,
|
||||||
assignees: filters.assignees ? filters.assignees.join(",") : undefined,
|
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
|
||||||
type: filters.type ? filters.type : undefined,
|
state: filters?.state ? filters?.state.join(",") : undefined,
|
||||||
labels: filters.labels ? filters.labels.join(",") : undefined,
|
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||||
issue__assignees__id: filters.issue__assignees__id
|
type: filters?.type ? filters?.type : undefined,
|
||||||
? filters.issue__assignees__id.join(",")
|
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,
|
: undefined,
|
||||||
issue__labels__id: filters.issue__labels__id ? filters.issue__labels__id.join(",") : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR(
|
const { data: projectIssues } = useSWR(
|
||||||
@ -60,6 +66,14 @@ const useIssuesView = () => {
|
|||||||
: null
|
: 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(
|
const { data: cycleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId && params
|
workspaceSlug && projectId && cycleId && params
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string)
|
||||||
@ -95,11 +109,11 @@ const useIssuesView = () => {
|
|||||||
[key: string]: IIssue[];
|
[key: string]: IIssue[];
|
||||||
}
|
}
|
||||||
| undefined = useMemo(() => {
|
| undefined = useMemo(() => {
|
||||||
const issuesToGroup = cycleIssues ?? moduleIssues ?? projectIssues;
|
const issuesToGroup = viewIssues ?? cycleIssues ?? moduleIssues ?? projectIssues;
|
||||||
|
|
||||||
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
||||||
else return issuesToGroup;
|
else return issuesToGroup;
|
||||||
}, [projectIssues, cycleIssues, moduleIssues]);
|
}, [projectIssues, cycleIssues, moduleIssues, viewIssues]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
|
@ -10,6 +10,8 @@ import projectService from "services/project.service";
|
|||||||
import viewsService from "services/views.service";
|
import viewsService from "services/views.service";
|
||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
|
// contexts
|
||||||
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateViewModal, DeleteViewModal } from "components/views";
|
import { CreateUpdateViewModal, DeleteViewModal } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
@ -18,6 +20,9 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|||||||
import { UserAuth } from "types";
|
import { UserAuth } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_DETAILS, VIEW_DETAILS, VIEW_ISSUES } from "constants/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 SingleView: React.FC<UserAuth> = (props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -42,15 +47,8 @@ const SingleView: React.FC<UserAuth> = (props) => {
|
|||||||
: null
|
: 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 (
|
return (
|
||||||
|
<IssueViewContextProvider>
|
||||||
<AppLayout
|
<AppLayout
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
@ -60,9 +58,25 @@ const SingleView: React.FC<UserAuth> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</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>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Content here
|
<IssuesView userAuth={props} />
|
||||||
</AppLayout>
|
</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> {
|
async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
|
||||||
return this.delete(`/api/projects/${projectId}/views/${viewId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
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 {
|
export interface IIssueFilterOptions {
|
||||||
type: "active" | "backlog" | null;
|
type: "active" | "backlog" | null;
|
||||||
assignees: string[] | null;
|
assignees: string[] | null;
|
||||||
|
state: string[] | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
issue__assignees__id: string[] | null;
|
issue__assignees__id: string[] | null;
|
||||||
issue__labels__id: string[] | null;
|
issue__labels__id: string[] | null;
|
||||||
|
priority: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueViewOptions {
|
export interface IIssueViewOptions {
|
||||||
|
11
apps/app/types/views.d.ts
vendored
11
apps/app/types/views.d.ts
vendored
@ -7,7 +7,13 @@ export interface IView {
|
|||||||
updated_by: string;
|
updated_by: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
query: {
|
query: IQuery;
|
||||||
|
query_data: IQuery;
|
||||||
|
project: string;
|
||||||
|
workspace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQuery {
|
||||||
state: string[] | null;
|
state: string[] | null;
|
||||||
parent: string[] | null;
|
parent: string[] | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
@ -29,7 +35,4 @@ export interface IView {
|
|||||||
target_date: string[] | null;
|
target_date: string[] | null;
|
||||||
completed_at: string[] | null;
|
completed_at: string[] | null;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
|
||||||
project: string;
|
|
||||||
workspace: string;
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user