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";
|
||||
// services
|
||||
import viewsService from "services/views.service";
|
||||
import workspaceService from "services/workspace.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
@ -17,16 +18,17 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { ICurrentUserResponse, IView } from "types";
|
||||
// fetch-keys
|
||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
||||
import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
viewType: "project" | "workspace";
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
data: IView | null;
|
||||
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 router = useRouter();
|
||||
@ -41,14 +43,15 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
||||
|
||||
const handleDeletion = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
if (viewType === "project") {
|
||||
if (!workspaceSlug || !data || !projectId) return;
|
||||
|
||||
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)
|
||||
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
|
||||
views?.filter((view) => view.id !== data.id)
|
||||
);
|
||||
|
||||
handleClose();
|
||||
@ -69,6 +72,35 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
||||
.finally(() => {
|
||||
setIsDeleteLoading(false);
|
||||
});
|
||||
} else {
|
||||
if (!workspaceSlug || !data) return;
|
||||
|
||||
await workspaceService
|
||||
.deleteView(workspaceSlug as string, data.id)
|
||||
.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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -8,6 +8,7 @@ import { mutate } from "swr";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import viewsService from "services/views.service";
|
||||
import workspaceService from "services/workspace.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
@ -15,10 +16,11 @@ import { ViewForm } from "components/views";
|
||||
// types
|
||||
import { ICurrentUserResponse, IView } from "types";
|
||||
// fetch-keys
|
||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
||||
import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
viewType: "project" | "workspace";
|
||||
handleClose: () => void;
|
||||
data?: IView | null;
|
||||
preLoadedData?: Partial<IView> | null;
|
||||
@ -27,6 +29,7 @@ type Props = {
|
||||
|
||||
export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
viewType,
|
||||
handleClose,
|
||||
data,
|
||||
preLoadedData,
|
||||
@ -46,6 +49,8 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||
...payload,
|
||||
query_data: payload.query,
|
||||
};
|
||||
|
||||
if (viewType === "project") {
|
||||
await viewsService
|
||||
.createView(workspaceSlug as string, projectId as string, payload, user)
|
||||
.then(() => {
|
||||
@ -65,6 +70,27 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||
message: "View could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await workspaceService
|
||||
.createView(workspaceSlug as string, payload)
|
||||
.then(() => {
|
||||
mutate(WORKSPACE_VIEWS_LIST(workspaceSlug 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.",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateView = async (payload: IView) => {
|
||||
@ -72,6 +98,7 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||
...payload,
|
||||
query_data: payload.query,
|
||||
};
|
||||
if (viewType === "project") {
|
||||
await viewsService
|
||||
.updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user)
|
||||
.then((res) => {
|
||||
@ -100,13 +127,50 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
||||
message: "View could not be updated. Please try again.",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await workspaceService
|
||||
.updateView(workspaceSlug as string, data?.id ?? "", payloadData)
|
||||
.then((res) => {
|
||||
mutate<IView[]>(
|
||||
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) => {
|
||||
if (viewType === "project") {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
if (!data) await createView(formData);
|
||||
else await updateView(formData);
|
||||
} else {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (!data) await createView(formData);
|
||||
else await updateView(formData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -5,9 +5,9 @@ import { useRouter } from "next/router";
|
||||
|
||||
// icons
|
||||
import { TrashIcon, StarIcon, PencilIcon } from "@heroicons/react/24/outline";
|
||||
import { StackedLayersIcon } from "components/icons";
|
||||
import { PhotoFilterOutlined } from "@mui/icons-material";
|
||||
//components
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
import { CustomMenu } from "components/ui";
|
||||
// services
|
||||
import viewsService from "services/views.service";
|
||||
// types
|
||||
@ -18,15 +18,20 @@ import { VIEWS_LIST } from "constants/fetch-keys";
|
||||
import useToast from "hooks/use-toast";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { renderShortDateWithYearFormat, render24HourFormatTime } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
view: IView;
|
||||
viewType: "project" | "workspace";
|
||||
handleEditView: () => 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 { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -82,19 +87,34 @@ 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 (
|
||||
<div className="first:rounded-t-[10px] last:rounded-b-[10px] hover:bg-custom-background-90">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
|
||||
<a>
|
||||
<div className="relative rounded p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<StackedLayersIcon height={18} width={18} />
|
||||
<p className="mr-2 truncate text-sm">{truncateText(view.name, 75)}</p>
|
||||
<div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
|
||||
<Link href={viewRedirectionUrl}>
|
||||
<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(view.name, 75)}
|
||||
</p>
|
||||
{view?.description && (
|
||||
<p className="text-xs text-custom-text-200">{view.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="rounded-full bg-custom-background-80 py-0.5 px-2 text-xs text-custom-text-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="rounded bg-custom-background-80 py-1 px-2 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100">
|
||||
{Object.keys(view.query_data)
|
||||
.map((key: string) =>
|
||||
view.query_data[key as keyof typeof view.query_data] !== null
|
||||
@ -104,16 +124,9 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
||||
.reduce((curr, prev) => curr + prev, 0)}{" "}
|
||||
filters
|
||||
</p>
|
||||
<Tooltip
|
||||
tooltipContent={`Last updated at ${render24HourFormatTime(
|
||||
view.updated_at
|
||||
)} ${renderShortDateWithYearFormat(view.updated_at)}`}
|
||||
>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{render24HourFormatTime(view.updated_at)}
|
||||
</p>
|
||||
</Tooltip>
|
||||
{view.is_favorite ? (
|
||||
|
||||
{viewType === "project" ? (
|
||||
view.is_favorite ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
@ -135,8 +148,9 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
||||
>
|
||||
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
||||
</button>
|
||||
)}
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
)
|
||||
) : null}
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
@ -165,12 +179,6 @@ export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDe
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{view?.description && (
|
||||
<p className="px-[27px] text-sm font-normal leading-5 text-custom-text-200">
|
||||
{view.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
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