feat: global issues

This commit is contained in:
Anmol Singh Bhatia 2023-09-22 15:22:05 +05:30
parent 8dbd77974b
commit e87890f8ac
11 changed files with 1587 additions and 136 deletions

View File

@ -0,0 +1,330 @@
import React from "react";
import { useRouter } from "next/router";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// hooks
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import useEstimateOption from "hooks/use-estimate-option";
// components
import { MyIssuesSelectFilters } from "components/issues";
// ui
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { FormatListBulletedOutlined } from "@mui/icons-material";
import { CreditCard } from "lucide-react";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { Properties, TIssueViewOptions } from "types";
// constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
},
{
type: "spreadsheet",
Icon: CreditCard,
},
];
export const WorkspaceIssuesViewOptions: React.FC = () => {
const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query;
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
useMyIssuesFilters(workspaceSlug?.toString());
const { isEstimateActive } = useEstimateOption();
const workspaceViewPathName = [
"workspace-views/all-issues",
"workspace-views/assigned",
"workspace-views/created",
"workspace-views/subscribed",
];
const isWorkspaceViewPath = workspaceViewPathName.some((pathname) =>
router.pathname.includes(pathname)
);
const showFilters = isWorkspaceViewPath || workspaceViewId;
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-x- px-1 py-0.5 rounded bg-custom-sidebar-background-90 ">
{issueViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom"
>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-100 duration-300 ${
displayFilters?.layout === option.type
? "bg-custom-sidebar-background-100 shadow-sm"
: "text-custom-sidebar-text-200"
}`}
onClick={() => {
setDisplayFilters({ layout: option.type });
if (option.type === "spreadsheet")
router.push(`/${workspaceSlug}/workspace-views/all-issues`);
else router.push(`/${workspaceSlug}/workspace-views`);
}}
>
<option.Icon
sx={{
fontSize: 16,
}}
className={option.type === "spreadsheet" ? "h-4 w-4" : ""}
/>
</button>
</Tooltip>
))}
</div>
{showFilters && (
<>
<MyIssuesSelectFilters
filters={filters}
onSelect={(option) => {
const key = option.key as keyof typeof filters;
if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters?.[key] ?? [],
option.value
);
setFilters({
[key]: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);
if (valueExists)
setFilters({
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
});
else
setFilters({
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
});
}
}}
direction="left"
height="rg"
/>
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
}`}
>
Display
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters?.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (
displayFilters?.layout === "spreadsheet" &&
option.key === null
)
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setDisplayFilters({ group_by: option.key })
}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters?.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (
displayFilters?.group_by === "priority" &&
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</div>
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setDisplayFilters({
type: option.key,
})
}
>
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
</div>
</>
)}
</div>
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
if (
displayFilters?.layout === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
)
return null;
if (
displayFilters?.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
return (
<button
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200"
}`}
onClick={() => setProperty(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</>
)}
</div>
);
};

View File

@ -8,6 +8,7 @@ import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
import viewsService from "services/views.service"; import viewsService from "services/views.service";
import workspaceService from "services/workspace.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
@ -17,16 +18,17 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// types // types
import type { ICurrentUserResponse, IView } from "types"; import type { ICurrentUserResponse, IView } from "types";
// fetch-keys // fetch-keys
import { VIEWS_LIST } from "constants/fetch-keys"; import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
viewType: "project" | "workspace";
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
data: IView | null; data: IView | null;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user }) => { export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, viewType, user }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter(); const router = useRouter();
@ -41,34 +43,64 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
const handleDeletion = async () => { const handleDeletion = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
if (!workspaceSlug || !data || !projectId) return;
await viewsService if (viewType === "project") {
.deleteView(workspaceSlug as string, projectId as string, data.id, user) if (!workspaceSlug || !data || !projectId) return;
.then(() => {
mutate<IView[]>(
VIEWS_LIST(projectId as string),
(views) => views?.filter((view) => view.id !== data.id)
);
handleClose(); await viewsService
.deleteView(workspaceSlug as string, projectId as string, data.id, user)
.then(() => {
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
views?.filter((view) => view.id !== data.id)
);
setToastAlert({ handleClose();
type: "success",
title: "Success!", setToastAlert({
message: "View deleted successfully.", type: "success",
title: "Success!",
message: "View deleted successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be deleted. Please try again.",
});
})
.finally(() => {
setIsDeleteLoading(false);
}); });
}) } else {
.catch(() => { if (!workspaceSlug || !data) return;
setToastAlert({
type: "error", await workspaceService
title: "Error!", .deleteView(workspaceSlug as string, data.id)
message: "View could not be deleted. Please try again.", .then(() => {
mutate<IView[]>(WORKSPACE_VIEWS_LIST(workspaceSlug as string), (views) =>
views?.filter((view) => view.id !== data.id)
);
handleClose();
setToastAlert({
type: "success",
title: "Success!",
message: "View deleted successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be deleted. Please try again.",
});
})
.finally(() => {
setIsDeleteLoading(false);
}); });
}) }
.finally(() => {
setIsDeleteLoading(false);
});
}; };
return ( return (

View File

@ -8,6 +8,7 @@ import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
import viewsService from "services/views.service"; import viewsService from "services/views.service";
import workspaceService from "services/workspace.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
@ -15,10 +16,11 @@ import { ViewForm } from "components/views";
// types // types
import { ICurrentUserResponse, IView } from "types"; import { ICurrentUserResponse, IView } from "types";
// fetch-keys // fetch-keys
import { VIEWS_LIST } from "constants/fetch-keys"; import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
viewType: "project" | "workspace";
handleClose: () => void; handleClose: () => void;
data?: IView | null; data?: IView | null;
preLoadedData?: Partial<IView> | null; preLoadedData?: Partial<IView> | null;
@ -27,6 +29,7 @@ type Props = {
export const CreateUpdateViewModal: React.FC<Props> = ({ export const CreateUpdateViewModal: React.FC<Props> = ({
isOpen, isOpen,
viewType,
handleClose, handleClose,
data, data,
preLoadedData, preLoadedData,
@ -46,25 +49,48 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
...payload, ...payload,
query_data: payload.query, query_data: payload.query,
}; };
await viewsService
.createView(workspaceSlug as string, projectId as string, payload, user)
.then(() => {
mutate(VIEWS_LIST(projectId as string));
handleClose();
setToastAlert({ if (viewType === "project") {
type: "success", await viewsService
title: "Success!", .createView(workspaceSlug as string, projectId as string, payload, user)
message: "View created successfully.", .then(() => {
mutate(VIEWS_LIST(projectId as string));
handleClose();
setToastAlert({
type: "success",
title: "Success!",
message: "View created successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be created. Please try again.",
});
}); });
}) } else {
.catch(() => { await workspaceService
setToastAlert({ .createView(workspaceSlug as string, payload)
type: "error", .then(() => {
title: "Error!", mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string));
message: "View could not be created. Please try again.", handleClose();
setToastAlert({
type: "success",
title: "Success!",
message: "View created successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be created. Please try again.",
});
}); });
}); }
}; };
const updateView = async (payload: IView) => { const updateView = async (payload: IView) => {
@ -72,41 +98,79 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
...payload, ...payload,
query_data: payload.query, query_data: payload.query,
}; };
await viewsService if (viewType === "project") {
.updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) await viewsService
.then((res) => { .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user)
mutate<IView[]>( .then((res) => {
VIEWS_LIST(projectId as string), mutate<IView[]>(
(prevData) => VIEWS_LIST(projectId as string),
prevData?.map((p) => { (prevData) =>
if (p.id === res.id) return { ...p, ...payloadData }; prevData?.map((p) => {
if (p.id === res.id) return { ...p, ...payloadData };
return p; return p;
}), }),
false false
); );
onClose(); onClose();
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "View updated successfully.", message: "View updated successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be updated. Please try again.",
});
}); });
}) } else {
.catch(() => { await workspaceService
setToastAlert({ .updateView(workspaceSlug as string, data?.id ?? "", payloadData)
type: "error", .then((res) => {
title: "Error!", mutate<IView[]>(
message: "View could not be updated. Please try again.", WORKSPACE_VIEWS_LIST(workspaceSlug as string),
(prevData) =>
prevData?.map((p) => {
if (p.id === res.id) return { ...p, ...payloadData };
return p;
}),
false
);
onClose();
setToastAlert({
type: "success",
title: "Success!",
message: "View updated successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "View could not be updated. Please try again.",
});
}); });
}); }
}; };
const handleFormSubmit = async (formData: IView) => { const handleFormSubmit = async (formData: IView) => {
if (!workspaceSlug || !projectId) return; if (viewType === "project") {
if (!workspaceSlug || !projectId) return;
if (!data) await createView(formData); if (!data) await createView(formData);
else await updateView(formData); else await updateView(formData);
} else {
if (!workspaceSlug) return;
if (!data) await createView(formData);
else await updateView(formData);
}
}; };
return ( return (

View File

@ -5,9 +5,9 @@ import { useRouter } from "next/router";
// icons // icons
import { TrashIcon, StarIcon, PencilIcon } from "@heroicons/react/24/outline"; import { TrashIcon, StarIcon, PencilIcon } from "@heroicons/react/24/outline";
import { StackedLayersIcon } from "components/icons"; import { PhotoFilterOutlined } from "@mui/icons-material";
//components //components
import { CustomMenu, Tooltip } from "components/ui"; import { CustomMenu } from "components/ui";
// services // services
import viewsService from "services/views.service"; import viewsService from "services/views.service";
// types // types
@ -18,15 +18,20 @@ import { VIEWS_LIST } from "constants/fetch-keys";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { renderShortDateWithYearFormat, render24HourFormatTime } from "helpers/date-time.helper";
type Props = { type Props = {
view: IView; view: IView;
viewType: "project" | "workspace";
handleEditView: () => void; handleEditView: () => void;
handleDeleteView: () => void; handleDeleteView: () => void;
}; };
export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDeleteView }) => { export const SingleViewItem: React.FC<Props> = ({
view,
viewType,
handleEditView,
handleDeleteView,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -82,38 +87,46 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
}); });
}; };
const viewRedirectionUrl =
viewType === "project"
? `/${workspaceSlug}/projects/${projectId}/views/${view.id}`
: `/${workspaceSlug}/workspace-views/${view.id}`;
return ( return (
<div className="first:rounded-t-[10px] last:rounded-b-[10px] hover:bg-custom-background-90"> <div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}> <Link href={viewRedirectionUrl}>
<a> <a className="flex items-center justify-between relative rounded px-5 py-4 w-full">
<div className="relative rounded p-4"> <div className="flex items-center justify-between w-full">
<div className="flex items-center justify-between"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div
<StackedLayersIcon height={18} width={18} /> className={`flex items-center justify-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100`}
<p className="mr-2 truncate text-sm">{truncateText(view.name, 75)}</p> >
<PhotoFilterOutlined className="!text-base !leading-6" />
</div> </div>
<div className="ml-2 flex flex-shrink-0"> <div className="flex flex-col">
<div className="flex items-center gap-2"> <p className="truncate text-sm leading-4 font-medium">
<p className="rounded-full bg-custom-background-80 py-0.5 px-2 text-xs text-custom-text-200"> {truncateText(view.name, 75)}
{Object.keys(view.query_data) </p>
.map((key: string) => {view?.description && (
view.query_data[key as keyof typeof view.query_data] !== null <p className="text-xs text-custom-text-200">{view.description}</p>
? (view.query_data[key as keyof typeof view.query_data] as any).length )}
: 0 </div>
) </div>
.reduce((curr, prev) => curr + prev, 0)}{" "} <div className="ml-2 flex flex-shrink-0">
filters <div className="flex items-center gap-4">
</p> <p className="rounded bg-custom-background-80 py-1 px-2 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100">
<Tooltip {Object.keys(view.query_data)
tooltipContent={`Last updated at ${render24HourFormatTime( .map((key: string) =>
view.updated_at view.query_data[key as keyof typeof view.query_data] !== null
)} ${renderShortDateWithYearFormat(view.updated_at)}`} ? (view.query_data[key as keyof typeof view.query_data] as any).length
> : 0
<p className="text-sm text-custom-text-200"> )
{render24HourFormatTime(view.updated_at)} .reduce((curr, prev) => curr + prev, 0)}{" "}
</p> filters
</Tooltip> </p>
{view.is_favorite ? (
{viewType === "project" ? (
view.is_favorite ? (
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
@ -135,41 +148,36 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
> >
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" /> <StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button> </button>
)} )
<CustomMenu width="auto" verticalEllipsis> ) : null}
<CustomMenu.MenuItem <CustomMenu width="auto" ellipsis>
onClick={(e: any) => { <CustomMenu.MenuItem
e.preventDefault(); onClick={(e: any) => {
e.stopPropagation(); e.preventDefault();
handleEditView(); e.stopPropagation();
}} handleEditView();
> }}
<span className="flex items-center justify-start gap-2"> >
<PencilIcon className="h-3.5 w-3.5" /> <span className="flex items-center justify-start gap-2">
<span>Edit View</span> <PencilIcon className="h-3.5 w-3.5" />
</span> <span>Edit View</span>
</CustomMenu.MenuItem> </span>
<CustomMenu.MenuItem </CustomMenu.MenuItem>
onClick={(e: any) => { <CustomMenu.MenuItem
e.preventDefault(); onClick={(e: any) => {
e.stopPropagation(); e.preventDefault();
handleDeleteView(); e.stopPropagation();
}} handleDeleteView();
> }}
<span className="flex items-center justify-start gap-2"> >
<TrashIcon className="h-3.5 w-3.5" /> <span className="flex items-center justify-start gap-2">
<span>Delete View</span> <TrashIcon className="h-3.5 w-3.5" />
</span> <span>Delete View</span>
</CustomMenu.MenuItem> </span>
</CustomMenu> </CustomMenu.MenuItem>
</div> </CustomMenu>
</div> </div>
</div> </div>
{view?.description && (
<p className="px-[27px] text-sm font-normal leading-5 text-custom-text-200">
{view.description}
</p>
)}
</div> </div>
</a> </a>
</Link> </Link>

View File

@ -0,0 +1,167 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// hooks
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import useToast from "hooks/use-toast";
// components
import { FiltersList } from "components/core";
import { PrimaryButton } from "components/ui";
import { CreateUpdateViewModal } from "components/views";
// icon
import { PlusIcon } from "lucide-react";
// constant
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
// service
import workspaceService from "services/workspace.service";
// type
import { ICurrentUserResponse, IIssueFilterOptions } from "types";
type Props = {
user: ICurrentUserResponse | undefined;
};
export const WorkspaceViewsNavigation: React.FC<Props> = ({ user }) => {
const [createViewModal, setCreateViewModal] = useState<any>(null);
const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query;
const { setToastAlert } = useToast();
const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
const { data: workspaceViews } = useSWR(
workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug.toString()) : null,
workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug.toString()) : null
);
const isSelected = (pathName: string) => router.pathname.includes(pathName);
const tabsList = [
{
key: "all",
label: "All Issues",
selected: isSelected("workspace-views/all-issues"),
onClick: () => router.push(`/${workspaceSlug}/workspace-views/all-issues`),
},
{
key: "assigned",
label: "Assigned",
selected: isSelected("workspace-views/assigned"),
onClick: () => router.push(`/${workspaceSlug}/workspace-views/assigned`),
},
{
key: "created",
label: "Created",
selected: isSelected("workspace-views/created"),
onClick: () => router.push(`/${workspaceSlug}/workspace-views/created`),
},
{
key: "subscribed",
label: "Subscribed",
selected: isSelected("workspace-views/subscribed"),
onClick: () => router.push(`/${workspaceSlug}/workspace-views/subscribed`),
},
];
const nullFilters = Object.keys(filters).filter(
(key) => filters[key as keyof IIssueFilterOptions] === null
);
const areFiltersApplied =
Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length;
return (
<>
<CreateUpdateViewModal
isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)}
viewType="workspace"
preLoadedData={createViewModal}
user={user}
/>
<div className="group flex items-center overflow-x-scroll">
{tabsList.map((tab) => (
<button
key={tab.key}
type="button"
onClick={tab.onClick}
className={`border-b-2 min-w-[96px] p-4 text-sm font-medium outline-none whitespace-nowrap ${
tab.selected
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent hover:border-custom-primary-100 hover:text-custom-primary-100"
}`}
>
{tab.label}
</button>
))}
{workspaceViews &&
workspaceViews.length > 0 &&
workspaceViews?.map((view) => (
<button
className={`border-b-2 min-w-[96px] p-4 text-sm font-medium outline-none whitespace-nowrap ${
view.id === workspaceViewId
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent hover:border-custom-primary-100 hover:text-custom-primary-100"
}`}
onClick={() => router.push(`/${workspaceSlug}/workspace-views/${view.id}`)}
>
{view.name}
</button>
))}
<button type="button" className="min-w-[96px] " onClick={() => setCreateViewModal(true)}>
<PlusIcon className="h-4 w-4 text-custom-primary-200 hover:text-current" />
</button>
</div>
{areFiltersApplied && (
<>
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
<FiltersList
filters={filters}
setFilters={(updatedFilter) => setFilters(updatedFilter)}
labels={[]}
members={[]}
states={[]}
clearAllFilters={() =>
setFilters({
assignees: null,
created_by: null,
labels: null,
priority: null,
state: null,
start_date: null,
target_date: null,
})
}
/>
<PrimaryButton
onClick={() => {
if (workspaceViewId) {
setFilters({});
setToastAlert({
title: "View updated",
message: "Your view has been updated",
type: "success",
});
} else
setCreateViewModal({
query: filters,
});
}}
className="flex items-center gap-2 text-sm"
>
{!workspaceViewId && <PlusIcon className="h-4 w-4" />}
{workspaceViewId ? "Update" : "Save"} view
</PrimaryButton>
</div>
{<div className="mt-3 border-t border-custom-border-200" />}
</>
)}
</>
);
};

View File

@ -0,0 +1,137 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// hook
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
import useUser from "hooks/use-user";
// services
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
// ui
import { EmptyState, PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyView from "public/empty-state/view.svg";
// fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
import { CheckCircle } from "lucide-react";
const WorkspaceView: React.FC = () => {
const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query;
const { user } = useUser();
const { data: viewDetails, error } = useSWR(
workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null,
workspaceSlug && workspaceViewId
? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString())
: null
);
const { displayFilters } = useMyIssuesFilters(workspaceSlug?.toString());
const params: any = {
assignees: viewDetails?.query_data?.assignees
? viewDetails?.query_data?.assignees.join(",")
: undefined,
state: viewDetails?.query_data?.state ? viewDetails?.query_data?.state.join(",") : undefined,
priority: viewDetails?.query_data?.priority
? viewDetails.query_data?.priority.join(",")
: undefined,
labels: viewDetails?.query_data?.labels ? viewDetails?.query_data?.labels.join(",") : undefined,
created_by: viewDetails?.query_data?.created_by
? viewDetails?.query_data?.created_by.join(",")
: undefined,
start_date: viewDetails?.query_data?.start_date
? viewDetails?.query_data?.start_date.join(",")
: undefined,
target_date: viewDetails?.query_data?.target_date
? viewDetails?.query_data?.target_date.join(",")
: undefined,
sub_issue: displayFilters?.sub_issue,
type: displayFilters?.type ? displayFilters?.type : undefined,
};
const { data: viewIssues, mutate: mutateIssues } = useSWR(
workspaceSlug && viewDetails ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null,
workspaceSlug && viewDetails
? () => workspaceService.getViewIssues(workspaceSlug.toString(), params)
: null
);
return (
<IssueViewContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
</span>
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation user={user} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}}
/>
) : (
<div className="h-full w-full flex flex-col">
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div>
</WorkspaceAuthorizationLayout>
</IssueViewContextProvider>
);
};
export default WorkspaceView;

View File

@ -0,0 +1,125 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// hook
import useUser from "hooks/use-user";
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
// services
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
// ui
import { EmptyState, PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
// images
import emptyView from "public/empty-state/view.svg";
// fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
const WorkspaceViewAllIssue: React.FC = () => {
const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query;
const { user } = useUser();
const { data: viewDetails, error } = useSWR(
workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null,
workspaceSlug && workspaceViewId
? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString())
: null
);
const { displayFilters } = useMyIssuesFilters(workspaceSlug?.toString());
const params: any = {
assignees: undefined,
state: undefined,
state_group: undefined,
subscriber: undefined,
priority: undefined,
labels: undefined,
created_by: undefined,
start_date: undefined,
target_date: undefined,
sub_issue: false,
type: displayFilters?.type ? displayFilters?.type : undefined,
};
const { data: viewIssues, mutate: mutateIssues } = useSWR(
workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null,
workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null
);
return (
<IssueViewContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
</span>
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation user={user} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}}
/>
) : (
<div className="h-full w-full flex flex-col">
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div>
</WorkspaceAuthorizationLayout>
</IssueViewContextProvider>
);
};
export default WorkspaceViewAllIssue;

View File

@ -0,0 +1,125 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// hook
import useUser from "hooks/use-user";
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
// services
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
// ui
import { EmptyState, PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
// images
import emptyView from "public/empty-state/view.svg";
// fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
const WorkspaceViewAssignedIssue: React.FC = () => {
const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query;
const { user } = useUser();
const { data: viewDetails, error } = useSWR(
workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null,
workspaceSlug && workspaceViewId
? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString())
: null
);
const { displayFilters } = useMyIssuesFilters(workspaceSlug?.toString());
const params: any = {
assignees: user?.id ?? undefined,
state: undefined,
state_group: undefined,
subscriber: undefined,
priority: undefined,
labels: undefined,
created_by: undefined,
start_date: undefined,
target_date: undefined,
sub_issue: false,
type: displayFilters?.type ? displayFilters?.type : undefined,
};
const { data: viewIssues, mutate: mutateIssues } = useSWR(
workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null,
workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null
);
return (
<IssueViewContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
</span>
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation user={user} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}}
/>
) : (
<div className="h-full w-full flex flex-col">
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div>
</WorkspaceAuthorizationLayout>
</IssueViewContextProvider>
);
};
export default WorkspaceViewAssignedIssue;

View File

@ -0,0 +1,125 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// hook
import useUser from "hooks/use-user";
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
// services
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
// ui
import { EmptyState, PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
// images
import emptyView from "public/empty-state/view.svg";
// fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
const WorkspaceViewCreatedIssue: React.FC = () => {
const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query;
const { user } = useUser();
const { data: viewDetails, error } = useSWR(
workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null,
workspaceSlug && workspaceViewId
? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString())
: null
);
const { displayFilters } = useMyIssuesFilters(workspaceSlug?.toString());
const params: any = {
assignees: undefined,
state: undefined,
state_group: undefined,
subscriber: undefined,
priority: undefined,
labels: undefined,
created_by: user?.id ?? undefined,
start_date: undefined,
target_date: undefined,
sub_issue: false,
type: displayFilters?.type ? displayFilters?.type : undefined,
};
const { data: viewIssues, mutate: mutateIssues } = useSWR(
workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null,
workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null
);
return (
<IssueViewContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
</span>
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation user={user} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}}
/>
) : (
<div className="h-full w-full flex flex-col">
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div>
</WorkspaceAuthorizationLayout>
</IssueViewContextProvider>
);
};
export default WorkspaceViewCreatedIssue;

View File

@ -0,0 +1,213 @@
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// hooks
import useUser from "hooks/use-user";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components
import { CreateUpdateViewModal, DeleteViewModal, SingleViewItem } from "components/views";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
// ui
import { EmptyState, Input, Loader, PrimaryButton } from "components/ui";
// icons
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { PlusIcon } from "lucide-react";
import { PhotoFilterOutlined } from "@mui/icons-material";
// image
import emptyView from "public/empty-state/view.svg";
// types
import type { NextPage } from "next";
import { IView } from "types";
// constants
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const WorkspaceViews: NextPage = () => {
console.log("test");
// const [currentView, setCurrentView] = useState("");
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IView | null>(null);
const [deleteViewModal, setDeleteViewModal] = useState(false);
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IView | null>(null);
const handleEditView = (view: IView) => {
setSelectedViewToUpdate(view);
setCreateUpdateViewModal(true);
};
const handleDeleteView = (view: IView) => {
setSelectedViewToDelete(view);
setDeleteViewModal(true);
};
const [query, setQuery] = useState("");
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: workspaceViews } = useSWR(
workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug as string) : null
);
const defaultWorkspaceViewsList = [
{
key: "all",
label: "All Issues",
href: `/${workspaceSlug}/workspace-views/all-issues`,
},
{
key: "assigned",
label: "Assigned",
href: `/${workspaceSlug}/workspace-views/assigned`,
},
{
key: "created",
label: "Created",
href: `/${workspaceSlug}/workspace-views/created`,
},
{
key: "subscribed",
label: "Subscribed",
href: `/${workspaceSlug}/workspace-views/subscribed`,
},
];
const filteredDefaultOptions =
query === ""
? defaultWorkspaceViewsList
: defaultWorkspaceViewsList?.filter((option) =>
option.label.toLowerCase().includes(query.toLowerCase())
);
const filteredOptions =
query === ""
? workspaceViews
: workspaceViews?.filter((option) => option.name.toLowerCase().includes(query.toLowerCase()));
const { user } = useUser();
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<span className="text-sm font-medium">Workspace Views</span>
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => setCreateUpdateViewModal(true)}
>
<PlusIcon className="h-4 w-4" />
New View
</PrimaryButton>
</div>
}
>
<CreateUpdateViewModal
isOpen={createUpdateViewModal}
handleClose={() => {
setCreateUpdateViewModal(false);
setSelectedViewToUpdate(null);
}}
data={selectedViewToUpdate}
viewType="workspace"
user={user}
/>
<DeleteViewModal
isOpen={deleteViewModal}
data={selectedViewToDelete}
setIsOpen={setDeleteViewModal}
viewType="workspace"
user={user}
/>
<div className="flex flex-col">
<div className="h-full w-full flex flex-col overflow-hidden">
<div className="flex items-center gap-2.5 w-full px-5 py-3 border-b border-custom-border-200">
<MagnifyingGlassIcon className="h-4 w-4 text-custom-text-200" />
<Input
className="w-full bg-transparent text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 !p-0 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
mode="trueTransparent"
/>
</div>
</div>
{filteredDefaultOptions &&
filteredDefaultOptions.length > 0 &&
filteredDefaultOptions.map((option) => (
<div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
<Link href={option.href}>
<a className="flex items-center justify-between relative rounded px-5 py-4 w-full">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4">
<div
className={`flex items-center justify-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100`}
>
<PhotoFilterOutlined className="!text-base !leading-6" />
</div>
<div className="flex flex-col">
<p className="truncate text-sm leading-4 font-medium">
{truncateText(option.label, 75)}
</p>
</div>
</div>
</div>
</a>
</Link>
</div>
))}
{filteredOptions ? (
filteredOptions.length > 0 ? (
<div>
{filteredOptions.map((view) => (
<SingleViewItem
key={view.id}
view={view}
viewType="workspace"
handleEditView={() => handleEditView(view)}
handleDeleteView={() => handleDeleteView(view)}
/>
))}
</div>
) : (
<EmptyState
title="Get focused with views"
description="Views aid in saving your issues by applying various filters and grouping options."
image={emptyView}
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New View",
onClick: () => setCreateUpdateViewModal(true),
}}
/>
)
) : (
<Loader className="space-y-2 p-5">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
)}
</div>
</WorkspaceAuthorizationLayout>
);
};
export default WorkspaceViews;

View File

@ -0,0 +1,125 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// hook
import useUser from "hooks/use-user";
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
// services
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { SpreadsheetView } from "components/core";
import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
// ui
import { EmptyState, PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
// images
import emptyView from "public/empty-state/view.svg";
// fetch-keys
import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys";
const WorkspaceViewSubscribedIssue: React.FC = () => {
const router = useRouter();
const { workspaceSlug, workspaceViewId } = router.query;
const { user } = useUser();
const { data: viewDetails, error } = useSWR(
workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null,
workspaceSlug && workspaceViewId
? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString())
: null
);
const { displayFilters } = useMyIssuesFilters(workspaceSlug?.toString());
const params: any = {
assignees: undefined,
state: undefined,
state_group: undefined,
subscriber: user?.id ?? undefined,
priority: undefined,
labels: undefined,
created_by: undefined,
start_date: undefined,
target_date: undefined,
sub_issue: false,
type: displayFilters?.type ? displayFilters?.type : undefined,
};
const { data: viewIssues, mutate: mutateIssues } = useSWR(
workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null,
workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null
);
return (
<IssueViewContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">
{viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"}
</span>
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full border-b border-custom-border-300">
<WorkspaceViewsNavigation user={user} />
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/workspace-views`),
}}
/>
) : (
<div className="h-full w-full flex flex-col">
<SpreadsheetView
spreadsheetIssues={viewIssues}
mutateIssues={mutateIssues}
handleIssueAction={(...args) => {}}
disableUserActions={false}
user={user}
userAuth={{
isGuest: false,
isMember: false,
isOwner: false,
isViewer: false,
}}
/>
</div>
)}
</div>
</div>
</WorkspaceAuthorizationLayout>
</IssueViewContextProvider>
);
};
export default WorkspaceViewSubscribedIssue;