From e87890f8acf904e95ae28c0d19093ea31d1fd053 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Fri, 22 Sep 2023 15:22:05 +0530 Subject: [PATCH] feat: global issues --- .../workspace-issue-view-option.tsx | 330 ++++++++++++++++++ web/components/views/delete-view-modal.tsx | 82 +++-- web/components/views/modal.tsx | 152 +++++--- web/components/views/single-view-item.tsx | 142 ++++---- .../views/workpace-view-navigation.tsx | 167 +++++++++ .../workspace-views/[workspaceViewId].tsx | 137 ++++++++ .../workspace-views/all-issues.tsx | 125 +++++++ .../workspace-views/assigned.tsx | 125 +++++++ .../workspace-views/created.tsx | 125 +++++++ .../[workspaceSlug]/workspace-views/index.tsx | 213 +++++++++++ .../workspace-views/subscribed.tsx | 125 +++++++ 11 files changed, 1587 insertions(+), 136 deletions(-) create mode 100644 web/components/issues/workspace-views/workspace-issue-view-option.tsx create mode 100644 web/components/workspace/views/workpace-view-navigation.tsx create mode 100644 web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx create mode 100644 web/pages/[workspaceSlug]/workspace-views/all-issues.tsx create mode 100644 web/pages/[workspaceSlug]/workspace-views/assigned.tsx create mode 100644 web/pages/[workspaceSlug]/workspace-views/created.tsx create mode 100644 web/pages/[workspaceSlug]/workspace-views/index.tsx create mode 100644 web/pages/[workspaceSlug]/workspace-views/subscribed.tsx diff --git a/web/components/issues/workspace-views/workspace-issue-view-option.tsx b/web/components/issues/workspace-views/workspace-issue-view-option.tsx new file mode 100644 index 000000000..0faa58ecd --- /dev/null +++ b/web/components/issues/workspace-views/workspace-issue-view-option.tsx @@ -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 ( +
+
+ {issueViewOptions.map((option) => ( + {replaceUnderscoreIfSnakeCase(option.type)} View + } + position="bottom" + > + + + ))} +
+ + {showFilters && ( + <> + { + 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" + /> + + {({ open }) => ( + <> + + Display + + + + +
+
+ {displayFilters?.layout !== "calendar" && + displayFilters?.layout !== "spreadsheet" && ( + <> +
+

Group by

+
+ 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 ( + + setDisplayFilters({ group_by: option.key }) + } + > + {option.name} + + ); + })} + +
+
+
+

Order by

+
+ 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 ( + { + setDisplayFilters({ order_by: option.key }); + }} + > + {option.name} + + ); + })} + +
+
+ + )} +
+

Issue type

+
+ option.key === displayFilters?.type + )?.name ?? "Select" + } + className="!w-full" + buttonClassName="w-full" + > + {FILTER_ISSUE_OPTIONS.map((option) => ( + + setDisplayFilters({ + type: option.key, + }) + } + > + {option.name} + + ))} + +
+
+ + {displayFilters?.layout !== "calendar" && + displayFilters?.layout !== "spreadsheet" && ( + <> +
+

Show empty states

+
+ + setDisplayFilters({ + show_empty_groups: !displayFilters?.show_empty_groups, + }) + } + /> +
+
+ + )} +
+ +
+

Display Properties

+
+ {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 ( + + ); + })} +
+
+
+
+
+ + )} +
+ + )} +
+ ); +}; diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index c65f7ba29..0d49c62cc 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -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>; data: IView | null; user: ICurrentUserResponse | undefined; }; -export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user }) => { +export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, viewType, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -41,34 +43,64 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user const handleDeletion = async () => { setIsDeleteLoading(true); - if (!workspaceSlug || !data || !projectId) return; - await viewsService - .deleteView(workspaceSlug as string, projectId as string, data.id, user) - .then(() => { - mutate( - VIEWS_LIST(projectId as string), - (views) => views?.filter((view) => view.id !== data.id) - ); + if (viewType === "project") { + if (!workspaceSlug || !data || !projectId) return; - handleClose(); + await viewsService + .deleteView(workspaceSlug as string, projectId as string, data.id, user) + .then(() => { + mutate(VIEWS_LIST(projectId as string), (views) => + views?.filter((view) => view.id !== data.id) + ); - setToastAlert({ - type: "success", - title: "Success!", - message: "View deleted successfully.", + 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); }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be deleted. Please try again.", + } else { + if (!workspaceSlug || !data) return; + + await workspaceService + .deleteView(workspaceSlug as string, data.id) + .then(() => { + mutate(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 ( diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index c1ff54231..88e69554d 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -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 | null; @@ -27,6 +29,7 @@ type Props = { export const CreateUpdateViewModal: React.FC = ({ isOpen, + viewType, handleClose, data, preLoadedData, @@ -46,25 +49,48 @@ export const CreateUpdateViewModal: React.FC = ({ ...payload, query_data: payload.query, }; - await viewsService - .createView(workspaceSlug as string, projectId as string, payload, user) - .then(() => { - mutate(VIEWS_LIST(projectId as string)); - handleClose(); - setToastAlert({ - type: "success", - title: "Success!", - message: "View created successfully.", + if (viewType === "project") { + await viewsService + .createView(workspaceSlug as string, projectId as string, payload, user) + .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.", + }); }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - 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,41 +98,79 @@ export const CreateUpdateViewModal: React.FC = ({ ...payload, query_data: payload.query, }; - await viewsService - .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) - .then((res) => { - mutate( - VIEWS_LIST(projectId as string), - (prevData) => - prevData?.map((p) => { - if (p.id === res.id) return { ...p, ...payloadData }; + if (viewType === "project") { + await viewsService + .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) + .then((res) => { + mutate( + VIEWS_LIST(projectId as string), + (prevData) => + prevData?.map((p) => { + if (p.id === res.id) return { ...p, ...payloadData }; - return p; - }), - false - ); - onClose(); + return p; + }), + false + ); + onClose(); - setToastAlert({ - type: "success", - title: "Success!", - message: "View updated successfully.", + setToastAlert({ + type: "success", + title: "Success!", + message: "View updated successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be updated. Please try again.", + }); }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be updated. Please try again.", + } else { + await workspaceService + .updateView(workspaceSlug as string, data?.id ?? "", payloadData) + .then((res) => { + mutate( + 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 (!workspaceSlug || !projectId) return; + if (viewType === "project") { + if (!workspaceSlug || !projectId) return; - if (!data) await createView(formData); - else await updateView(formData); + if (!data) await createView(formData); + else await updateView(formData); + } else { + if (!workspaceSlug) return; + + if (!data) await createView(formData); + else await updateView(formData); + } }; return ( diff --git a/web/components/views/single-view-item.tsx b/web/components/views/single-view-item.tsx index a6f81912c..d27eb3cf1 100644 --- a/web/components/views/single-view-item.tsx +++ b/web/components/views/single-view-item.tsx @@ -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 = ({ view, handleEditView, handleDeleteView }) => { +export const SingleViewItem: React.FC = ({ + view, + viewType, + handleEditView, + handleDeleteView, +}) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -82,38 +87,46 @@ export const SingleViewItem: React.FC = ({ view, handleEditView, handleDe }); }; + const viewRedirectionUrl = + viewType === "project" + ? `/${workspaceSlug}/projects/${projectId}/views/${view.id}` + : `/${workspaceSlug}/workspace-views/${view.id}`; + return ( -
- - -
-
-
- -

{truncateText(view.name, 75)}

+
+ + +
+
+
+
-
-
-

- {Object.keys(view.query_data) - .map((key: string) => - view.query_data[key as keyof typeof view.query_data] !== null - ? (view.query_data[key as keyof typeof view.query_data] as any).length - : 0 - ) - .reduce((curr, prev) => curr + prev, 0)}{" "} - filters -

- -

- {render24HourFormatTime(view.updated_at)} -

-
- {view.is_favorite ? ( +
+

+ {truncateText(view.name, 75)} +

+ {view?.description && ( +

{view.description}

+ )} +
+
+
+
+

+ {Object.keys(view.query_data) + .map((key: string) => + view.query_data[key as keyof typeof view.query_data] !== null + ? (view.query_data[key as keyof typeof view.query_data] as any).length + : 0 + ) + .reduce((curr, prev) => curr + prev, 0)}{" "} + filters +

+ + {viewType === "project" ? ( + view.is_favorite ? ( - )} - - { - e.preventDefault(); - e.stopPropagation(); - handleEditView(); - }} - > - - - Edit View - - - { - e.preventDefault(); - e.stopPropagation(); - handleDeleteView(); - }} - > - - - Delete View - - - -
+ ) + ) : null} + + { + e.preventDefault(); + e.stopPropagation(); + handleEditView(); + }} + > + + + Edit View + + + { + e.preventDefault(); + e.stopPropagation(); + handleDeleteView(); + }} + > + + + Delete View + + +
- {view?.description && ( -

- {view.description} -

- )}
diff --git a/web/components/workspace/views/workpace-view-navigation.tsx b/web/components/workspace/views/workpace-view-navigation.tsx new file mode 100644 index 000000000..d492bda0f --- /dev/null +++ b/web/components/workspace/views/workpace-view-navigation.tsx @@ -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 = ({ user }) => { + const [createViewModal, setCreateViewModal] = useState(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 ( + <> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
+ {tabsList.map((tab) => ( + + ))} + {workspaceViews && + workspaceViews.length > 0 && + workspaceViews?.map((view) => ( + + ))} + + +
+ {areFiltersApplied && ( + <> +
+ setFilters(updatedFilter)} + labels={[]} + members={[]} + states={[]} + clearAllFilters={() => + setFilters({ + assignees: null, + created_by: null, + labels: null, + priority: null, + state: null, + start_date: null, + target_date: null, + }) + } + /> + { + 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 && } + {workspaceViewId ? "Update" : "Save"} view + +
+ {
} + + )} + + ); +}; diff --git a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx new file mode 100644 index 000000000..fb3c35cf6 --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx @@ -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 ( + + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
+ } + right={ +
+ + + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+
+ + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
+ {}} + disableUserActions={false} + user={user} + userAuth={{ + isGuest: false, + isMember: false, + isOwner: false, + isViewer: false, + }} + /> +
+ )} +
+
+ + + ); +}; + +export default WorkspaceView; diff --git a/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx b/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx new file mode 100644 index 000000000..dae32eb87 --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx @@ -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 ( + + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
+ } + right={ +
+ + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+
+ + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
+ {}} + disableUserActions={false} + user={user} + userAuth={{ + isGuest: false, + isMember: false, + isOwner: false, + isViewer: false, + }} + /> +
+ )} +
+
+ + + ); +}; + +export default WorkspaceViewAllIssue; diff --git a/web/pages/[workspaceSlug]/workspace-views/assigned.tsx b/web/pages/[workspaceSlug]/workspace-views/assigned.tsx new file mode 100644 index 000000000..b67f8e033 --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/assigned.tsx @@ -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 ( + + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
+ } + right={ +
+ + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+
+ + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
+ {}} + disableUserActions={false} + user={user} + userAuth={{ + isGuest: false, + isMember: false, + isOwner: false, + isViewer: false, + }} + /> +
+ )} +
+
+ + + ); +}; + +export default WorkspaceViewAssignedIssue; diff --git a/web/pages/[workspaceSlug]/workspace-views/created.tsx b/web/pages/[workspaceSlug]/workspace-views/created.tsx new file mode 100644 index 000000000..32fcec9ca --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/created.tsx @@ -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 ( + + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
+ } + right={ +
+ + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+
+ + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
+ {}} + disableUserActions={false} + user={user} + userAuth={{ + isGuest: false, + isMember: false, + isOwner: false, + isViewer: false, + }} + /> +
+ )} +
+
+ + + ); +}; + +export default WorkspaceViewCreatedIssue; diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx new file mode 100644 index 000000000..efec5d9fa --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -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(null); + + const [deleteViewModal, setDeleteViewModal] = useState(false); + const [selectedViewToDelete, setSelectedViewToDelete] = useState(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 ( + + Workspace Views +
+ } + right={ +
+ + + setCreateUpdateViewModal(true)} + > + + New View + +
+ } + > + { + setCreateUpdateViewModal(false); + setSelectedViewToUpdate(null); + }} + data={selectedViewToUpdate} + viewType="workspace" + user={user} + /> + +
+
+
+ + setQuery(e.target.value)} + placeholder="Search" + mode="trueTransparent" + /> +
+
+ {filteredDefaultOptions && + filteredDefaultOptions.length > 0 && + filteredDefaultOptions.map((option) => ( + + ))} + + {filteredOptions ? ( + filteredOptions.length > 0 ? ( +
+ {filteredOptions.map((view) => ( + handleEditView(view)} + handleDeleteView={() => handleDeleteView(view)} + /> + ))} +
+ ) : ( + , + text: "New View", + onClick: () => setCreateUpdateViewModal(true), + }} + /> + ) + ) : ( + + + + + + )} +
+ + ); +}; + +export default WorkspaceViews; diff --git a/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx b/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx new file mode 100644 index 000000000..e970cf384 --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx @@ -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 ( + + + + + {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} + +
+ } + right={ +
+ + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+
+ + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
+ {}} + disableUserActions={false} + user={user} + userAuth={{ + isGuest: false, + isMember: false, + isOwner: false, + isViewer: false, + }} + /> +
+ )} +
+
+ + + ); +}; + +export default WorkspaceViewSubscribedIssue;