forked from github/plane
feat: global issues
This commit is contained in:
parent
8dbd77974b
commit
e87890f8ac
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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 (
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
167
web/components/workspace/views/workpace-view-navigation.tsx
Normal file
167
web/components/workspace/views/workpace-view-navigation.tsx
Normal 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" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
137
web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx
Normal file
137
web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx
Normal 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;
|
125
web/pages/[workspaceSlug]/workspace-views/all-issues.tsx
Normal file
125
web/pages/[workspaceSlug]/workspace-views/all-issues.tsx
Normal 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;
|
125
web/pages/[workspaceSlug]/workspace-views/assigned.tsx
Normal file
125
web/pages/[workspaceSlug]/workspace-views/assigned.tsx
Normal 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;
|
125
web/pages/[workspaceSlug]/workspace-views/created.tsx
Normal file
125
web/pages/[workspaceSlug]/workspace-views/created.tsx
Normal 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;
|
213
web/pages/[workspaceSlug]/workspace-views/index.tsx
Normal file
213
web/pages/[workspaceSlug]/workspace-views/index.tsx
Normal 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;
|
125
web/pages/[workspaceSlug]/workspace-views/subscribed.tsx
Normal file
125
web/pages/[workspaceSlug]/workspace-views/subscribed.tsx
Normal 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;
|
Loading…
Reference in New Issue
Block a user