From eab7d9a10cc209471ceb17ecc6edb283b98dd8b0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 27 Sep 2023 20:29:04 +0530 Subject: [PATCH 1/6] chore: spreadsheet view context --- .../core/views/spreadsheet-view/index.ts | 1 - .../spreadsheet-view/spreadsheet-columns.tsx | 274 ------------ .../spreadsheet-view/spreadsheet-view.tsx | 354 +++++++++++++++- .../workspace-issue-view-option.tsx | 13 +- web/components/ui/dropdowns/custom-menu.tsx | 8 +- web/components/ui/dropdowns/types.d.ts | 1 + web/components/views/modal.tsx | 100 ++--- web/constants/fetch-keys.ts | 9 +- web/contexts/workspace-view-context.tsx | 155 +++++++ web/hooks/use-workspace-issues-view.tsx | 103 +++++ web/hooks/use-worskpace-issue-filter.tsx | 159 +++---- .../workspace-views/[workspaceViewId].tsx | 399 +++++++++--------- web/services/workspace.service.ts | 14 +- web/types/view-props.d.ts | 35 +- web/types/workspace-views.d.ts | 22 + 15 files changed, 1001 insertions(+), 646 deletions(-) delete mode 100644 web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx create mode 100644 web/contexts/workspace-view-context.tsx create mode 100644 web/hooks/use-workspace-issues-view.tsx create mode 100644 web/types/workspace-views.d.ts diff --git a/web/components/core/views/spreadsheet-view/index.ts b/web/components/core/views/spreadsheet-view/index.ts index e72819dad2b..9bf8ed1b0bd 100644 --- a/web/components/core/views/spreadsheet-view/index.ts +++ b/web/components/core/views/spreadsheet-view/index.ts @@ -10,5 +10,4 @@ export * from "./state-column"; export * from "./updated-on-column"; export * from "./spreadsheet-view"; export * from "./issue-column/issue-column"; -export * from "./spreadsheet-columns"; export * from "./issue-column/spreadsheet-issue-column"; diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx deleted file mode 100644 index f52f1ab3843..00000000000 --- a/web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import React from "react"; -// hooks -import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; -import useLocalStorage from "hooks/use-local-storage"; -// component -import { CustomMenu, Icon } from "components/ui"; -// icon -import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; -// types -import { TIssueOrderByOptions } from "types"; - -type Props = { - columnData: any; - gridTemplateColumns: string; -}; - -export const SpreadsheetColumns: React.FC = ({ columnData, gridTemplateColumns }) => { - const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage( - "spreadsheetViewSorting", - "" - ); - const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = - useLocalStorage("spreadsheetViewActiveSortingProperty", ""); - - const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView(); - - const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => { - setDisplayFilters({ order_by: order }); - setSelectedMenuItem(`${order}_${itemKey}`); - setActiveSortingProperty(order === "-created_at" ? "" : itemKey); - }; - - return ( -
- {columnData.map((col: any) => { - if (col.isActive) { - return ( -
- {col.propertyName === "title" ? ( -
- {col.colName} -
- ) : ( - - {activeSortingProperty === col.propertyName && ( -
- -
- )} - - {col.icon ? ( -
- } - width="xl" - > - { - handleOrderBy(col.ascendingOrder, col.propertyName); - }} - > -
-
- {col.propertyName === "assignee" || col.propertyName === "labels" ? ( - <> - - - - - A - - Z - - ) : col.propertyName === "due_date" || - col.propertyName === "created_on" || - col.propertyName === "updated_on" ? ( - <> - - - - - New - - Old - - ) : ( - <> - - - - - First - - Last - - )} -
- - -
-
- { - handleOrderBy(col.descendingOrder, col.propertyName); - }} - > -
-
- {col.propertyName === "assignee" || col.propertyName === "labels" ? ( - <> - - - - - Z - - A - - ) : col.propertyName === "due_date" ? ( - <> - - - - - Old - - New - - ) : ( - <> - - - - - Last - - First - - )} -
- - -
-
- {selectedMenuItem && - selectedMenuItem !== "" && - displayFilters?.order_by !== "-created_at" && - selectedMenuItem.includes(col.propertyName) && ( - { - handleOrderBy("-created_at", col.propertyName); - }} - > -
-
- - - - - Clear sorting -
-
-
- )} - - )} -
- ); - } - })} - - ); -}; diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 66dc97f2c8a..8c96bfadcc8 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; // next import { useRouter } from "next/router"; @@ -19,12 +19,19 @@ import { SpreadsheetStateColumn, SpreadsheetUpdatedOnColumn, } from "components/core"; -import { CustomMenu, Spinner } from "components/ui"; +import { CustomMenu, Icon, Spinner } from "components/ui"; import { IssuePeekOverview } from "components/issues"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; +import useLocalStorage from "hooks/use-local-storage"; // types -import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + ISubIssueResponse, + TIssueOrderByOptions, + UserAuth, +} from "types"; import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; import { CYCLE_DETAILS, @@ -39,7 +46,8 @@ import { import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import projectIssuesServices from "services/issues.service"; // icon -import { PlusIcon } from "lucide-react"; +import { CheckIcon, ChevronDownIcon, PlusIcon } from "lucide-react"; +import useWorkspaceIssuesView from "hooks/use-workspace-issues-view"; type Props = { spreadsheetIssues: IIssue[]; @@ -70,6 +78,10 @@ export const SpreadsheetView: React.FC = ({ const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + + const containerRef = useRef(null); + const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query; @@ -77,6 +89,13 @@ export const SpreadsheetView: React.FC = ({ const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); + const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage( + "spreadsheetViewSorting", + "" + ); + const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = + useLocalStorage("spreadsheetViewActiveSortingProperty", ""); + const workspaceIssuesPath = [ { params: { @@ -111,12 +130,18 @@ export const SpreadsheetView: React.FC = ({ router.pathname.includes(path.path) ); - const { params: workspaceViewParams } = useWorkspaceIssuesFilters( - workspaceSlug?.toString(), - workspaceViewId?.toString() - ); + // const { params: workspaceViewParams } = useWorkspaceIssuesFilters( + // workspaceSlug?.toString(), + // workspaceViewId?.toString() + // ); - const { params } = useSpreadsheetIssuesView(); + const { + params: workspaceViewParams, + displayFilters: workspaceDisplayFilters, + setDisplayFilters: setWorkspaceDisplayFilters, + } = useWorkspaceIssuesView(); + + const { params, displayFilters, setDisplayFilters } = useSpreadsheetIssuesView(); const partialUpdateIssue = useCallback( (formData: Partial, issue: IIssue) => { @@ -208,10 +233,216 @@ export const SpreadsheetView: React.FC = ({ const isNotAllowed = userAuth.isGuest || userAuth.isViewer; - const renderColumn = (header: string, Component: React.ComponentType) => ( + const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => { + if (!workspaceViewId || !currentWorkspaceIssuePath) + setWorkspaceDisplayFilters({ order_by: order }); + else setDisplayFilters({ order_by: order }); + setSelectedMenuItem(`${order}_${itemKey}`); + setActiveSortingProperty(order === "-created_at" ? "" : itemKey); + }; + + const renderColumn = ( + header: string, + propertyName: string, + Component: React.ComponentType, + ascendingOrder: TIssueOrderByOptions, + descendingOrder: TIssueOrderByOptions + ) => (
- {header} + + {activeSortingProperty === propertyName && ( +
+ +
+ )} + + {header} +
+ } + width="xl" + > + { + handleOrderBy(ascendingOrder, propertyName); + }} + > +
+
+ {propertyName === "assignee" || propertyName === "labels" ? ( + <> + + + + + A + + Z + + ) : propertyName === "due_date" || + propertyName === "created_on" || + propertyName === "updated_on" ? ( + <> + + + + + New + + Old + + ) : ( + <> + + + + + First + + Last + + )} +
+ + +
+
+ { + handleOrderBy(descendingOrder, propertyName); + }} + > +
+
+ {propertyName === "assignee" || propertyName === "labels" ? ( + <> + + + + + Z + + A + + ) : propertyName === "due_date" ? ( + <> + + + + + Old + + New + + ) : ( + <> + + + + + Last + + First + + )} +
+ + +
+
+ {selectedMenuItem && + selectedMenuItem !== "" && + displayFilters?.order_by !== "-created_at" && + selectedMenuItem.includes(propertyName) && ( + { + handleOrderBy("-created_at", propertyName); + }} + > +
+
+ + + + + Clear sorting +
+
+
+ )} +
{spreadsheetIssues.map((issue: IIssue, index) => ( @@ -230,6 +461,27 @@ export const SpreadsheetView: React.FC = ({
); + const handleScroll = () => { + if (containerRef.current) { + const scrollLeft = containerRef.current.scrollLeft; + setIsScrolled(scrollLeft > 0); + } + }; + + useEffect(() => { + const currentContainerRef = containerRef.current; + + if (currentContainerRef) { + currentContainerRef.addEventListener("scroll", handleScroll); + } + + return () => { + if (currentContainerRef) { + currentContainerRef.removeEventListener("scroll", handleScroll); + } + }; + }, []); + return ( <> = ({ />
-
+
{spreadsheetIssues ? ( <>
-
+
ID @@ -270,15 +526,69 @@ export const SpreadsheetView: React.FC = ({ ))}
- {renderColumn("State", SpreadsheetStateColumn)} - {renderColumn("Priority", SpreadsheetPriorityColumn)} - {renderColumn("Assignees", SpreadsheetAssigneeColumn)} - {renderColumn("Label", SpreadsheetLabelColumn)} - {renderColumn("Start Date", SpreadsheetStartDateColumn)} - {renderColumn("Due Date", SpreadsheetDueDateColumn)} - {renderColumn("Estimate", SpreadsheetEstimateColumn)} - {renderColumn("Created On", SpreadsheetCreatedOnColumn)} - {renderColumn("Updated On", SpreadsheetUpdatedOnColumn)} + {renderColumn( + "State", + "state", + SpreadsheetStateColumn, + "state__name", + "-state__name" + )} + {renderColumn( + "Priority", + "priority", + SpreadsheetPriorityColumn, + "priority", + "-priority" + )} + {renderColumn( + "Assignees", + "assignee", + SpreadsheetAssigneeColumn, + "assignees__first_name", + "-assignees__first_name" + )} + {renderColumn( + "Label", + "labels", + SpreadsheetLabelColumn, + "labels__name", + "-labels__name" + )} + {renderColumn( + "Start Date", + "start_date", + SpreadsheetStartDateColumn, + "-start_date", + "start_date" + )} + {renderColumn( + "Due Date", + "due_date", + SpreadsheetDueDateColumn, + "-target_date", + "target_date" + )} + {renderColumn( + "Estimate", + "estimate", + SpreadsheetEstimateColumn, + "estimate_point", + "-estimate_point" + )} + {renderColumn( + "Created On", + "created_on", + SpreadsheetCreatedOnColumn, + "-created_at", + "created_at" + )} + {renderColumn( + "Updated On", + "updated_on", + SpreadsheetUpdatedOnColumn, + "-updated_at", + "updated_at" + )} ) : (
diff --git a/web/components/issues/workspace-views/workspace-issue-view-option.tsx b/web/components/issues/workspace-views/workspace-issue-view-option.tsx index 4e98cce9214..adfff64ac08 100644 --- a/web/components/issues/workspace-views/workspace-issue-view-option.tsx +++ b/web/components/issues/workspace-views/workspace-issue-view-option.tsx @@ -17,6 +17,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types import { TIssueViewOptions } from "types"; +import useWorkspaceIssuesView from "hooks/use-workspace-issues-view"; const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ { @@ -33,12 +34,14 @@ export const WorkspaceIssuesViewOptions: React.FC = () => { const router = useRouter(); const { workspaceSlug, workspaceViewId } = router.query; - const { displayFilters, setDisplayFilters } = useMyIssuesFilters(workspaceSlug?.toString()); + // const { displayFilters, setDisplayFilters } = useMyIssuesFilters(workspaceSlug?.toString()); - const { filters, setFilters } = useWorkspaceIssuesFilters( - workspaceSlug?.toString(), - workspaceViewId?.toString() - ); + // const { filters, setFilters } = useWorkspaceIssuesFilters( + // workspaceSlug?.toString(), + // workspaceViewId?.toString() + // ); + + const { filters, setFilters, displayFilters, setDisplayFilters } = useWorkspaceIssuesView(); const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues"); diff --git a/web/components/ui/dropdowns/custom-menu.tsx b/web/components/ui/dropdowns/custom-menu.tsx index c451d443254..41450b2b35b 100644 --- a/web/components/ui/dropdowns/custom-menu.tsx +++ b/web/components/ui/dropdowns/custom-menu.tsx @@ -19,6 +19,7 @@ export type CustomMenuProps = DropdownProps & { const CustomMenu = ({ buttonClassName = "", + customButtonClassName = "", children, className = "", customButton, @@ -40,7 +41,12 @@ const CustomMenu = ({ {({ open }) => ( <> {customButton ? ( - + {customButton} ) : ( diff --git a/web/components/ui/dropdowns/types.d.ts b/web/components/ui/dropdowns/types.d.ts index aace1858a32..b368a7ed8d3 100644 --- a/web/components/ui/dropdowns/types.d.ts +++ b/web/components/ui/dropdowns/types.d.ts @@ -1,5 +1,6 @@ export type DropdownProps = { buttonClassName?: string; + customButtonClassName?: string; className?: string; customButton?: JSX.Element; disabled?: boolean; diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index 03f4f0b60da..93fa72321e1 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -17,6 +17,7 @@ import { ViewForm } from "components/views"; import { ICurrentUserResponse, IView } from "types"; // fetch-keys import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; +import { IWorkspaceView } from "types/workspace-views"; type Props = { isOpen: boolean; @@ -50,47 +51,51 @@ export const CreateUpdateViewModal: React.FC = ({ query_data: payload.query, }; - if (viewType === "project") { - await viewsService - .createView(workspaceSlug as string, projectId as string, payload, user) - .then(() => { - mutate(VIEWS_LIST(projectId as string)); - handleClose(); + 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.", - }); + setToastAlert({ + type: "success", + title: "Success!", + message: "View created successfully.", }); - } else { - await workspaceService - .createView(workspaceSlug as string, payload) - .then(() => { - mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string)); - handleClose(); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be created. Please try again.", + }); + }); + }; + const createWorkspaceView = async (payload: IWorkspaceView) => { + payload = { + ...payload, + query_data: payload.query, + }; - setToastAlert({ - type: "success", - title: "Success!", - message: "View created successfully.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be created. Please try again.", - }); + 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) => { @@ -159,18 +164,19 @@ export const CreateUpdateViewModal: React.FC = ({ } }; - 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; + const handleFormSubmit = async (formData: any) => { + if (!workspaceSlug) return; - if (!data) await createView(formData); - else await updateView(formData); + if (!data) { + if (viewType === "project") await createView(formData); + else createWorkspaceView(formData); } + // else await updateView(formData); + // } else if { + // if (!workspaceSlug) return; + + // if (!data) await createView(formData); + // else await updateView(formData); }; return ( diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 75107a0bba4..0ca6d80f52e 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -159,12 +159,11 @@ export const WORKSPACE_VIEWS_LIST = (workspaceSlug: string) => `WORKSPACE_VIEWS_LIST_${workspaceSlug.toUpperCase()}`; export const WORKSPACE_VIEW_DETAILS = (workspaceViewId: string) => `WORKSPACE_VIEW_DETAILS_${workspaceViewId.toUpperCase()}`; -export const WORKSPACE_VIEW_ISSUES = (workspaceViewId: string, params?: any) => { +export const WORKSPACE_VIEW_ISSUES = (workspaceViewId: string, params: string | null) => { if (!params) return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}`; - - const paramsKey = paramsToKey(params); - - return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}_${paramsKey.toUpperCase()}`; + return `WORKSPACE_VIEW_ISSUES_${workspaceViewId.toUpperCase()}_${paramsToKey( + params + ).toUpperCase()}`; }; export const PROJECT_ISSUES_DETAILS = (issueId: string) => diff --git a/web/contexts/workspace-view-context.tsx b/web/contexts/workspace-view-context.tsx new file mode 100644 index 00000000000..b462cbb9f47 --- /dev/null +++ b/web/contexts/workspace-view-context.tsx @@ -0,0 +1,155 @@ +import { createContext, useCallback, useEffect, useState } from "react"; +// next imports +import { useRouter } from "next/router"; +// swr +import useSWR from "swr"; +// services +import workspaceService from "services/workspace.service"; +// types +import { IWorkspaceViewProps } from "types"; +// fetch-keys +import { WORKSPACE_VIEW_DETAILS } from "constants/fetch-keys"; + +export const workspaceIssueViewContext = createContext({} as any); + +export const initialState: IWorkspaceViewProps = { + filters: { + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + subscriber: null, + start_date: null, + target_date: null, + project: null, + }, + display_filters: { + order_by: "-created_at", + sub_issue: true, + type: null, + }, + display_properties: { + assignee: true, + start_date: true, + due_date: true, + key: true, + labels: true, + priority: true, + state: true, + sub_issue_count: true, + attachment_count: true, + link: true, + estimate: true, + created_on: true, + updated_on: true, + }, +}; + +const saveViewFilters = async (workspaceSlug: string, workspaceViewId: string, state: any) => { + await workspaceService.updateView(workspaceSlug, workspaceViewId, { + query_data: state, + }); +}; + +export const WorkspaceIssueViewContextProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const [filters, setFilters] = useState(); + + const { data: workspaceViewFilters, mutate: mutateWorkspaceViewFilters } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + : null + ); + + useEffect(() => { + if (workspaceViewFilters && workspaceViewFilters?.query_data) { + const payload = { + filters: { + ...workspaceViewFilters?.query_data?.filters, + assignees: workspaceViewFilters?.query_data?.filters?.assignees || null, + created_by: workspaceViewFilters?.query_data?.filters?.created_by || null, + labels: workspaceViewFilters?.query_data?.filters?.labels || null, + priority: workspaceViewFilters?.query_data?.filters?.priority || null, + state_group: workspaceViewFilters?.query_data?.filters?.state_group || null, + subscriber: workspaceViewFilters?.query_data?.filters?.subscriber || null, + start_date: workspaceViewFilters?.query_data?.filters?.start_date || null, + target_date: workspaceViewFilters?.query_data?.filters?.target_date || null, + project: workspaceViewFilters?.query_data?.filters?.project || null, + }, + display_filters: { + ...workspaceViewFilters?.query_data?.display_filters, + order_by: workspaceViewFilters?.query_data?.display_filters?.order_by || "-created_at", + sub_issue: workspaceViewFilters?.query_data?.display_filters?.sub_issue || true, + type: workspaceViewFilters?.query_data?.display_filters?.type || null, + }, + display_properties: { + ...workspaceViewFilters?.query_data?.display_properties, + assignee: workspaceViewFilters?.query_data?.display_properties?.assignee || true, + start_date: workspaceViewFilters?.query_data?.display_properties?.start_date || true, + due_date: workspaceViewFilters?.query_data?.display_properties?.due_date || true, + key: workspaceViewFilters?.query_data?.display_properties?.key || true, + labels: workspaceViewFilters?.query_data?.display_properties?.labels || true, + priority: workspaceViewFilters?.query_data?.display_properties?.priority || true, + state: workspaceViewFilters?.query_data?.display_properties?.state || true, + sub_issue_count: + workspaceViewFilters?.query_data?.display_properties?.sub_issue_count || true, + attachment_count: + workspaceViewFilters?.query_data?.display_properties?.attachment_count || true, + link: workspaceViewFilters?.query_data?.display_properties?.link || true, + estimate: workspaceViewFilters?.query_data?.display_properties?.estimate || true, + created_on: workspaceViewFilters?.query_data?.display_properties?.created_on || true, + updated_on: workspaceViewFilters?.query_data?.display_properties?.updated_on || true, + }, + }; + + setFilters(payload); + } + }, [workspaceViewFilters, setFilters]); + + const handleFilters = ( + filter_type: "filters" | "display_filters" | "display_properties", + key: string, + value: string[] | boolean + ) => { + if (!workspaceSlug || !workspaceViewId) return; + + setFilters((prevData: IWorkspaceViewProps) => { + console.log("hello"); + return { ...prevData }; + }); + + mutateWorkspaceViewFilters((prevData: any) => { + if (!prevData) return prevData; + return { + ...prevData, + query_data: { + ...prevData.query_data, + [filter_type]: { + ...prevData.query_data[filter_type], + [key]: value, + }, + }, + }; + }, false); + + saveViewFilters(workspaceSlug as string, workspaceViewId as string, { + ...state, + [filter_type]: { + ...state[filter_type], + [key]: value, + }, + }); + }; + + return ( + + {children} + + ); +}; diff --git a/web/hooks/use-workspace-issues-view.tsx b/web/hooks/use-workspace-issues-view.tsx new file mode 100644 index 00000000000..d7851593351 --- /dev/null +++ b/web/hooks/use-workspace-issues-view.tsx @@ -0,0 +1,103 @@ +import { useContext } from "react"; +// next router +import { useRouter } from "next/router"; +// swr +import useSWR from "swr"; +// contexts +import { workspaceIssueViewContext } from "contexts/workspace-view-context"; +// services +import workspaceService from "services/workspace.service"; +// fetch-keys +import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; + +const useWorkspaceIssuesView = () => { + const { state, setFilters } = useContext(workspaceIssueViewContext); + console.log("state", state); + + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const computedFilter = (filters: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" + ? filters[key] + : filters[key].join(","); + }); + return computedFilters; + }; + + const params: any = { + assignees: (state && state?.filters?.assignees) || undefined, + created_by: (state && state?.filters?.created_by) || undefined, + labels: (state && state?.filters?.labels) || undefined, + priority: (state && state?.filters?.priority) || undefined, + state_group: (state && state?.filters?.state_group) || undefined, + subscriber: (state && state?.filters?.subscriber) || undefined, + start_date: (state && state?.filters?.start_date) || undefined, + target_date: (state && state?.filters?.target_date) || undefined, + project: (state && state?.filters?.project) || undefined, + order_by: (state && state?.display_filters?.order_by) || "-created_at", + sub_issue: (state && state?.display_filters?.sub_issue) || false, + type: state && state?.display_filters?.type, + }; + + // current view details + const { + data: view, + error: viewError, + isLoading: viewLoading, + } = useSWR( + workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + : null + ); + + // current view issues + const { + data: viewIssues, + mutate: mutateViewIssues, + isLoading: viewIssueLoading, + } = useSWR( + workspaceSlug && workspaceViewId + ? WORKSPACE_VIEW_ISSUES(workspaceViewId.toString(), params) + : null, + workspaceSlug && workspaceViewId + ? () => workspaceService.getViewIssues(workspaceViewId.toString(), computedFilter(params)) + : null + ); + + // console.log("----"); + // console.log("view", view); + // console.log("viewError", viewError); + // console.log("viewLoading", viewLoading); + // console.log("state", state); + + // console.log("viewIssues", viewIssues); + // console.log("viewIssueLoading", viewIssueLoading); + // console.log("mutateViewIssues", mutateViewIssues); + + // console.log("state", state); + // console.log("setFilters", setFilters); + // console.log("computedFilter(params)", computedFilter(params)); + + // console.log("----"); + + return { + view, + viewError, + viewLoading, + + viewIssues, + viewIssueLoading, + mutateViewIssues, + + state, + setFilters, + }; +}; + +export default useWorkspaceIssuesView; diff --git a/web/hooks/use-worskpace-issue-filter.tsx b/web/hooks/use-worskpace-issue-filter.tsx index 00af0a9f8b4..a2d627b8ff4 100644 --- a/web/hooks/use-worskpace-issue-filter.tsx +++ b/web/hooks/use-worskpace-issue-filter.tsx @@ -8,18 +8,40 @@ import workspaceService from "services/workspace.service"; import { IIssueFilterOptions, IView } from "types"; // fetch-keys import { WORKSPACE_VIEW_DETAILS } from "constants/fetch-keys"; - -const initialValues: IIssueFilterOptions = { - assignees: null, - created_by: null, - labels: null, - priority: null, - state: null, - state_group: null, - subscriber: null, - start_date: null, - target_date: null, - project: null, +import { IWorkspaceQuery, IWorkspaceView } from "types/workspace-views"; + +const initialValues: IWorkspaceQuery = { + filters: { + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + subscriber: null, + start_date: null, + target_date: null, + project: null, + }, + display_filters: { + order_by: "-created_at", + sub_issue: true, + type: null, + }, + display_properties: { + assignee: true, + start_date: true, + due_date: true, + key: true, + labels: true, + priority: true, + state: true, + sub_issue_count: true, + attachment_count: true, + link: true, + estimate: true, + created_on: true, + updated_on: true, + }, }; const useWorkspaceIssuesFilters = ( @@ -32,82 +54,73 @@ const useWorkspaceIssuesFilters = ( ? () => workspaceService.getViewDetails(workspaceSlug, workspaceViewId) : null ); - - const saveData = useCallback( - (data: Partial) => { - if (!workspaceSlug || !workspaceViewId || !workspaceViewDetails) return; - - const oldData = { ...workspaceViewDetails }; - - mutate( - WORKSPACE_VIEW_DETAILS(workspaceViewId), - (prevData) => { - if (!prevData) return; - return { - ...prevData, - query_data: { - ...prevData?.query_data, - ...data, - }, - }; - }, - false - ); - - workspaceService.updateView(workspaceSlug, workspaceViewId, { - query_data: { - ...oldData.query_data, - ...data, - }, - }); - }, - [workspaceViewDetails, workspaceSlug, workspaceViewId] - ); - - const filters = workspaceViewDetails?.query_data ?? initialValues; - - const setFilters = useCallback( - (updatedFilter: Partial) => { - if (!workspaceViewDetails) return; - - saveData({ - ...workspaceViewDetails?.query_data, - ...updatedFilter, - }); - }, - [workspaceViewDetails, saveData] - ); - + // const saveData = useCallback( + // (data: Partial) => { + // if (!workspaceSlug || !workspaceViewId || !workspaceViewDetails) return; + // const oldData = { ...workspaceViewDetails }; + // mutate( + // WORKSPACE_VIEW_DETAILS(workspaceViewId), + // (prevData) => { + // if (!prevData) return; + // return { + // ...prevData, + // query_data: { + // ...prevData?.query_data, + // ...data, + // }, + // }; + // }, + // false + // ); + // workspaceService.updateView(workspaceSlug, workspaceViewId, { + // query_data: { + // ...oldData.query_data, + // ...data, + // }, + // }); + // }, + // [workspaceViewDetails, workspaceSlug, workspaceViewId] + // ); + const filters = workspaceViewDetails?.query_data?.filters ?? initialValues.filters; + const displayFilters = + workspaceViewDetails?.query_data?.display_filters ?? initialValues.display_filters; + // const setFilters = useCallback( + // (updatedFilter: Partial) => { + // if (!workspaceViewDetails) return; + // saveData({ + // ...workspaceViewDetails?.query_data, + // ...updatedFilter, + // }); + // }, + // [workspaceViewDetails, saveData] + // ); useEffect(() => { if (!workspaceViewDetails || !workspaceSlug || !workspaceViewId) return; - if (!workspaceViewDetails.query_data) { workspaceService.updateView(workspaceSlug, workspaceViewId, { query_data: { ...initialValues }, }); } }, [workspaceViewDetails, workspaceViewId, workspaceSlug]); - const params: any = { assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, - subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined, - state: filters?.state ? filters?.state.join(",") : undefined, - state_group: filters?.state_group ? filters?.state_group.join(",") : undefined, - priority: filters?.priority ? filters?.priority.join(",") : undefined, - labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + labels: filters?.labels ? filters?.labels.join(",") : undefined, + priority: filters?.priority ? filters?.priority.join(",") : undefined, + state_group: filters?.state_group ? filters?.state_group.join(",") : undefined, + subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined, start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, project: filters?.project ? filters?.project.join(",") : undefined, - sub_issue: false, - type: undefined, - }; - - return { - params, - filters, - setFilters, + order_by: displayFilters?.order_by, + sub_issue: displayFilters?.sub_issue, + type: displayFilters?.type, }; + // return { + // params, + // filters, + // setFilters, + // }; }; export default useWorkspaceIssuesFilters; diff --git a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx index cba4d802b25..3dba8fbaadb 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx @@ -2,18 +2,19 @@ import { useCallback, useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; // hook -import useToast from "hooks/use-toast"; -import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; +// import useToast from "hooks/use-toast"; import useProjects from "hooks/use-projects"; import useUser from "hooks/use-user"; import useWorkspaceMembers from "hooks/use-workspace-members"; +import useWorkspaceIssuesView from "hooks/use-workspace-issues-view"; + // context import { useProjectMyMembership } from "contexts/project-member.context"; +import { WorkspaceIssueViewContextProvider } from "contexts/workspace-view-context"; // services -import workspaceService from "services/workspace.service"; import projectIssuesServices from "services/issues.service"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; @@ -31,18 +32,27 @@ import { CheckCircle } from "lucide-react"; // images import emptyView from "public/empty-state/view.svg"; // fetch-keys -import { - WORKSPACE_LABELS, - WORKSPACE_VIEWS_LIST, - WORKSPACE_VIEW_DETAILS, - WORKSPACE_VIEW_ISSUES, -} from "constants/fetch-keys"; +import { WORKSPACE_LABELS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; // constant import { STATE_GROUP } from "constants/project"; // types -import { IIssue, IIssueFilterOptions, IView } from "types"; +import { IIssue, IWorkspaceIssueFilterOptions } from "types"; +import workspaceService from "services/workspace.service"; const WorkspaceView: React.FC = () => { + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { memberRole } = useProjectMyMembership(); + const { user } = useUser(); + const { isGuest, isViewer } = useWorkspaceMembers( + workspaceSlug?.toString(), + Boolean(workspaceSlug) + ); + const { state } = useWorkspaceIssuesView(); + + console.log("state", state); + const [createViewModal, setCreateViewModal] = useState(null); // create issue modal @@ -61,38 +71,6 @@ const WorkspaceView: React.FC = () => { const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [issueToDelete, setIssueToDelete] = useState(null); - const router = useRouter(); - const { workspaceSlug, workspaceViewId } = router.query; - - const { memberRole } = useProjectMyMembership(); - - const { user } = useUser(); - const { setToastAlert } = useToast(); - - const { data: viewDetails, error } = useSWR( - workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, - workspaceSlug && workspaceViewId - ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) - : null - ); - - const { params, filters, setFilters } = useWorkspaceIssuesFilters( - workspaceSlug?.toString(), - workspaceViewId?.toString() - ); - - const { isGuest, isViewer } = useWorkspaceMembers( - workspaceSlug?.toString(), - Boolean(workspaceSlug) - ); - - const { data: viewIssues, mutate: mutateIssues } = useSWR( - workspaceSlug && viewDetails ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, - workspaceSlug && viewDetails - ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) - : null - ); - const { projects: allProjects } = useProjects(); const joinedProjects = allProjects?.filter((p) => p.is_member); @@ -103,38 +81,38 @@ const WorkspaceView: React.FC = () => { const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - const updateView = async (payload: IIssueFilterOptions) => { - const payloadData = { - query_data: payload, - }; + // const updateView = async (payload: Partial) => { + // const payloadData = { + // query_data: payload, + // }; - await workspaceService - .updateView(workspaceSlug as string, workspaceViewId as string, payloadData) - .then((res) => { - mutate( - WORKSPACE_VIEWS_LIST(workspaceSlug as string), - (prevData) => - prevData?.map((p) => { - if (p.id === res.id) return { ...p, ...payloadData }; + // await workspaceService + // .updateView(workspaceSlug as string, workspaceViewId as string, 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 - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "View updated successfully.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be updated. Please try again.", - }); - }); - }; + // return p; + // }), + // false + // ); + // 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 makeIssueCopy = useCallback( (issue: IIssue) => { @@ -175,147 +153,154 @@ const WorkspaceView: React.FC = () => { [makeIssueCopy, handleEditIssue, handleDeleteIssue] ); - const nullFilters = - filters && - Object.keys(filters).filter((key) => filters[key as keyof IIssueFilterOptions] === null); + // const nullFilters = + // filters && + // Object.keys(filters).filter( + // (key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null + // ); + + // const areFiltersApplied = + // filters && + // Object.keys(filters).length > 0 && + // nullFilters.length !== Object.keys(filters).length; - const areFiltersApplied = - filters && - Object.keys(filters).length > 0 && - nullFilters.length !== Object.keys(filters).length; + // const isNotAllowed = isGuest || isViewer; - const isNotAllowed = isGuest || isViewer; + // console.log("workspaceViewIssues", workspaceViewIssues); return ( - - - - {viewDetails ? `${viewDetails.name} Issues` : "Workspace Issues"} - -
- } - right={ -
- - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - -
- } - > - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => { - mutateIssues(); - }} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => { - mutateIssues(); - }} - /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateIssues(); - }} - /> - setCreateViewModal(null)} - viewType="workspace" - preLoadedData={createViewModal} - user={user} - /> -
-
- setCreateViewModal(true)} /> - {error ? ( - router.push(`/${workspaceSlug}/workspace-views`), + + {/* + + + {workspaceViewDetails ? `${workspaceViewDetails.name} Issues` : "Workspace Issues"} + +
+ } + right={ +
+ + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); }} - /> - ) : ( -
- {areFiltersApplied && ( - <> -
- setFilters(updatedFilter)} - labels={workspaceLabels} - members={workspaceMembers?.map((m) => m.member)} - stateGroup={STATE_GROUP} - project={joinedProjects} - clearAllFilters={() => - setFilters({ - assignees: null, - created_by: null, - labels: null, - priority: null, - state_group: null, - start_date: null, - target_date: null, - subscriber: null, - project: null, - }) - } - /> - { - if (workspaceViewId) { - updateView(filters); - } else - setCreateViewModal({ - query: filters, - }); - }} - className="flex items-center gap-2 text-sm" - > - {!workspaceViewId && } - {workspaceViewId ? "Update" : "Save"} view - -
- {
} - - )} - + + Add Issue + +
+ } + > + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateWorkspaceIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateWorkspaceIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateWorkspaceIssues(); + }} + /> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
+
+ setCreateViewModal(true)} /> + {error ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} /> -
- )} + ) : ( +
+ {true && ( + <> +
+ setFilters(updatedFilter)} + labels={workspaceLabels} + members={workspaceMembers?.map((m) => m.member)} + stateGroup={STATE_GROUP} + project={joinedProjects} + clearAllFilters={() => + setFilters({ + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + start_date: null, + target_date: null, + subscriber: null, + project: null, + }) + } + /> + { + if (workspaceViewId) { + // updateView(filters); + console.log("update"); + } else + setCreateViewModal({ + query: filters, + }); + }} + className="flex items-center gap-2 text-sm" + > + {!workspaceViewId && } + {workspaceViewId ? "Update" : "Save"} view + +
+ {
} + + )} + +
+ )} +
-
- + */} + ); }; diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index a3aa565070c..1ea294ad29e 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -14,9 +14,9 @@ import { ICurrentUserResponse, IWorkspaceBulkInviteFormData, IWorkspaceViewProps, - IView, IIssueFilterOptions, } from "types"; +import { IWorkspaceView } from "types/workspace-views"; class WorkspaceService extends APIService { constructor() { @@ -264,7 +264,7 @@ class WorkspaceService extends APIService { }); } - async createView(workspaceSlug: string, data: IView): Promise { + async createView(workspaceSlug: string, data: IWorkspaceView): Promise { return this.post(`/api/workspaces/${workspaceSlug}/views/`, data) .then((response) => response?.data) .catch((error) => { @@ -272,7 +272,11 @@ class WorkspaceService extends APIService { }); } - async updateView(workspaceSlug: string, viewId: string, data: Partial): Promise { + async updateView( + workspaceSlug: string, + viewId: string, + data: Partial + ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data) .then((response) => response?.data) .catch((error) => { @@ -288,7 +292,7 @@ class WorkspaceService extends APIService { }); } - async getAllViews(workspaceSlug: string): Promise { + async getAllViews(workspaceSlug: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/views/`) .then((response) => response?.data) .catch((error) => { @@ -296,7 +300,7 @@ class WorkspaceService extends APIService { }); } - async getViewDetails(workspaceSlug: string, viewId: string): Promise { + async getViewDetails(workspaceSlug: string, viewId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`) .then((response) => response?.data) .catch((error) => { diff --git a/web/types/view-props.d.ts b/web/types/view-props.d.ts index ca5ce47abca..c65225d21d6 100644 --- a/web/types/view-props.d.ts +++ b/web/types/view-props.d.ts @@ -1,3 +1,5 @@ +import { Properties } from "./workspace"; + export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; export type TIssueGroupByOptions = @@ -12,19 +14,22 @@ export type TIssueGroupByOptions = export type TIssueOrderByOptions = | "-created_at" + | "created_at" + | "updated_at" | "-updated_at" | "priority" + | "-priority" | "sort_order" | "state__name" | "-state__name" - | "assignees__name" - | "-assignees__name" + | "assignees__first_name" + | "-assignees__first_name" | "labels__name" | "-labels__name" | "target_date" | "-target_date" - | "estimate__point" - | "-estimate__point" + | "estimate_point" + | "-estimate_point" | "start_date" | "-start_date"; @@ -52,13 +57,31 @@ export interface IIssueDisplayFilterOptions { type?: "active" | "backlog" | null; } +export interface IWorkspaceIssueFilterOptions { + assignees?: string[] | null; + created_by?: string[] | null; + labels?: string[] | null; + priority?: string[] | null; + state_group?: string[] | null; + subscriber?: string[] | null; + start_date?: string[] | null; + target_date?: string[] | null; + project?: string[] | null; +} + +export interface IWorkspaceIssueDisplayFilterOptions { + order_by?: string | undefined; + type?: "active" | "backlog" | null; + sub_issue?: boolean; +} + export interface IProjectViewProps { display_filters: IIssueDisplayFilterOptions | undefined; filters: IIssueFilterOptions; } export interface IWorkspaceViewProps { - display_filters: IIssueDisplayFilterOptions | undefined; + filters: IWorkspaceIssueFilterOptions; + display_filters: IWorkspaceIssueDisplayFilterOptions | undefined; display_properties: Properties | undefined; - filters: IIssueFilterOptions; } diff --git a/web/types/workspace-views.d.ts b/web/types/workspace-views.d.ts new file mode 100644 index 00000000000..ec7f430d798 --- /dev/null +++ b/web/types/workspace-views.d.ts @@ -0,0 +1,22 @@ +import { IWorkspaceViewProps } from "./view-props"; + +export interface IWorkspaceView { + id: string; + access: string; + created_at: Date; + updated_at: Date; + is_favorite: boolean; + created_by: string; + updated_by: string; + name: string; + description: string; + query: IWorkspaceViewProps; + query_data: IWorkspaceViewProps; + project: string; + workspace: string; + workspace_detail?: { + id: string; + name: string; + slug: string; + }; +} From 6c23594784fa336bb26f28222f39a3f27f8f7903 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Wed, 27 Sep 2023 21:03:07 +0530 Subject: [PATCH 2/6] chore: spreadsheet context provider --- web/contexts/workspace-view-context.tsx | 189 ++++++++++-------- web/hooks/use-workspace-issues-view.tsx | 104 +--------- .../workspace-views/[workspaceViewId].tsx | 13 +- web/pages/_app.tsx | 18 +- 4 files changed, 128 insertions(+), 196 deletions(-) diff --git a/web/contexts/workspace-view-context.tsx b/web/contexts/workspace-view-context.tsx index b462cbb9f47..51ea22af5b7 100644 --- a/web/contexts/workspace-view-context.tsx +++ b/web/contexts/workspace-view-context.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useEffect, useState } from "react"; +import { createContext, useEffect, useState } from "react"; // next imports import { useRouter } from "next/router"; // swr @@ -8,9 +8,21 @@ import workspaceService from "services/workspace.service"; // types import { IWorkspaceViewProps } from "types"; // fetch-keys -import { WORKSPACE_VIEW_DETAILS } from "constants/fetch-keys"; +import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; -export const workspaceIssueViewContext = createContext({} as any); +export interface IWorkspaceViewContext { + view: any; + viewIssues: any; + filters: IWorkspaceViewProps; + handleFilters: ( + filterType: "filters" | "display_filters" | "display_properties", + payload: { [key: string]: any } + ) => void; +} + +export const WorkspaceIssueViewContext = createContext( + undefined +); export const initialState: IWorkspaceViewProps = { filters: { @@ -46,110 +58,121 @@ export const initialState: IWorkspaceViewProps = { }, }; -const saveViewFilters = async (workspaceSlug: string, workspaceViewId: string, state: any) => { - await workspaceService.updateView(workspaceSlug, workspaceViewId, { - query_data: state, - }); -}; - -export const WorkspaceIssueViewContextProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { +export const WorkspaceViewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const router = useRouter(); const { workspaceSlug, workspaceViewId } = router.query; - const [filters, setFilters] = useState(); + const [filters, setFilters] = useState(initialState); + const handleFilters = ( + filterType: "filters" | "display_filters" | "display_properties", + payload: { [key: string]: any } + ) => { + setFilters((prevData: IWorkspaceViewProps) => ({ + ...prevData, + [filterType]: { + ...prevData[filterType], + ...payload, + }, + })); + }; + + const computedFilter = (filters: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" + ? filters[key] + : filters[key].join(","); + }); + return computedFilters; + }; - const { data: workspaceViewFilters, mutate: mutateWorkspaceViewFilters } = useSWR( + const params: any = { + assignees: (filters && filters?.filters?.assignees) || undefined, + created_by: (filters && filters?.filters?.created_by) || undefined, + labels: (filters && filters?.filters?.labels) || undefined, + priority: (filters && filters?.filters?.priority) || undefined, + state_group: (filters && filters?.filters?.state_group) || undefined, + subscriber: (filters && filters?.filters?.subscriber) || undefined, + start_date: (filters && filters?.filters?.start_date) || undefined, + target_date: (filters && filters?.filters?.target_date) || undefined, + project: (filters && filters?.filters?.project) || undefined, + order_by: (filters && filters?.display_filters?.order_by) || "-created_at", + sub_issue: (filters && filters?.display_filters?.sub_issue) || false, + type: filters && filters?.display_filters?.type, + }; + + const { data: view, isLoading: viewLoading } = useSWR( workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, workspaceSlug && workspaceViewId ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) : null ); + const { data: viewIssues, isLoading: viewIssueLoading } = useSWR( + workspaceSlug && view && workspaceViewId + ? WORKSPACE_VIEW_ISSUES(workspaceViewId.toString(), params) + : null, + workspaceSlug && view && workspaceViewId + ? () => workspaceService.getViewIssues(workspaceViewId.toString(), computedFilter(params)) + : null + ); + + console.log("view", view); + console.log("viewIssues", viewIssues); + useEffect(() => { - if (workspaceViewFilters && workspaceViewFilters?.query_data) { + if (view && view?.query_data) { const payload = { filters: { - ...workspaceViewFilters?.query_data?.filters, - assignees: workspaceViewFilters?.query_data?.filters?.assignees || null, - created_by: workspaceViewFilters?.query_data?.filters?.created_by || null, - labels: workspaceViewFilters?.query_data?.filters?.labels || null, - priority: workspaceViewFilters?.query_data?.filters?.priority || null, - state_group: workspaceViewFilters?.query_data?.filters?.state_group || null, - subscriber: workspaceViewFilters?.query_data?.filters?.subscriber || null, - start_date: workspaceViewFilters?.query_data?.filters?.start_date || null, - target_date: workspaceViewFilters?.query_data?.filters?.target_date || null, - project: workspaceViewFilters?.query_data?.filters?.project || null, + ...view?.query_data?.filters, + assignees: view?.query_data?.filters?.assignees || null, + created_by: view?.query_data?.filters?.created_by || null, + labels: view?.query_data?.filters?.labels || null, + priority: view?.query_data?.filters?.priority || null, + state_group: view?.query_data?.filters?.state_group || null, + subscriber: view?.query_data?.filters?.subscriber || null, + start_date: view?.query_data?.filters?.start_date || null, + target_date: view?.query_data?.filters?.target_date || null, + project: view?.query_data?.filters?.project || null, }, display_filters: { - ...workspaceViewFilters?.query_data?.display_filters, - order_by: workspaceViewFilters?.query_data?.display_filters?.order_by || "-created_at", - sub_issue: workspaceViewFilters?.query_data?.display_filters?.sub_issue || true, - type: workspaceViewFilters?.query_data?.display_filters?.type || null, + ...view?.query_data?.display_filters, + order_by: view?.query_data?.display_filters?.order_by || "-created_at", + sub_issue: view?.query_data?.display_filters?.sub_issue || true, + type: view?.query_data?.display_filters?.type || null, }, display_properties: { - ...workspaceViewFilters?.query_data?.display_properties, - assignee: workspaceViewFilters?.query_data?.display_properties?.assignee || true, - start_date: workspaceViewFilters?.query_data?.display_properties?.start_date || true, - due_date: workspaceViewFilters?.query_data?.display_properties?.due_date || true, - key: workspaceViewFilters?.query_data?.display_properties?.key || true, - labels: workspaceViewFilters?.query_data?.display_properties?.labels || true, - priority: workspaceViewFilters?.query_data?.display_properties?.priority || true, - state: workspaceViewFilters?.query_data?.display_properties?.state || true, - sub_issue_count: - workspaceViewFilters?.query_data?.display_properties?.sub_issue_count || true, - attachment_count: - workspaceViewFilters?.query_data?.display_properties?.attachment_count || true, - link: workspaceViewFilters?.query_data?.display_properties?.link || true, - estimate: workspaceViewFilters?.query_data?.display_properties?.estimate || true, - created_on: workspaceViewFilters?.query_data?.display_properties?.created_on || true, - updated_on: workspaceViewFilters?.query_data?.display_properties?.updated_on || true, + ...view?.query_data?.display_properties, + assignee: view?.query_data?.display_properties?.assignee || true, + start_date: view?.query_data?.display_properties?.start_date || true, + due_date: view?.query_data?.display_properties?.due_date || true, + key: view?.query_data?.display_properties?.key || true, + labels: view?.query_data?.display_properties?.labels || true, + priority: view?.query_data?.display_properties?.priority || true, + state: view?.query_data?.display_properties?.state || true, + sub_issue_count: view?.query_data?.display_properties?.sub_issue_count || true, + attachment_count: view?.query_data?.display_properties?.attachment_count || true, + link: view?.query_data?.display_properties?.link || true, + estimate: view?.query_data?.display_properties?.estimate || true, + created_on: view?.query_data?.display_properties?.created_on || true, + updated_on: view?.query_data?.display_properties?.updated_on || true, }, }; - setFilters(payload); } - }, [workspaceViewFilters, setFilters]); - - const handleFilters = ( - filter_type: "filters" | "display_filters" | "display_properties", - key: string, - value: string[] | boolean - ) => { - if (!workspaceSlug || !workspaceViewId) return; - - setFilters((prevData: IWorkspaceViewProps) => { - console.log("hello"); - return { ...prevData }; - }); - - mutateWorkspaceViewFilters((prevData: any) => { - if (!prevData) return prevData; - return { - ...prevData, - query_data: { - ...prevData.query_data, - [filter_type]: { - ...prevData.query_data[filter_type], - [key]: value, - }, - }, - }; - }, false); + }, [view, setFilters]); - saveViewFilters(workspaceSlug as string, workspaceViewId as string, { - ...state, - [filter_type]: { - ...state[filter_type], - [key]: value, - }, - }); - }; + // const saveViewFilters = async (workspaceSlug: string, workspaceViewId: string, state: any) => { + // await workspaceService.updateView(workspaceSlug, workspaceViewId, { + // query_data: state, + // }); + // }; return ( - + {children} - + ); }; diff --git a/web/hooks/use-workspace-issues-view.tsx b/web/hooks/use-workspace-issues-view.tsx index d7851593351..e183bc63388 100644 --- a/web/hooks/use-workspace-issues-view.tsx +++ b/web/hooks/use-workspace-issues-view.tsx @@ -1,103 +1,11 @@ import { useContext } from "react"; -// next router -import { useRouter } from "next/router"; -// swr -import useSWR from "swr"; -// contexts -import { workspaceIssueViewContext } from "contexts/workspace-view-context"; -// services -import workspaceService from "services/workspace.service"; -// fetch-keys -import { WORKSPACE_VIEW_DETAILS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; +// types +import { IWorkspaceViewContext, WorkspaceIssueViewContext } from "contexts/workspace-view-context"; -const useWorkspaceIssuesView = () => { - const { state, setFilters } = useContext(workspaceIssueViewContext); - console.log("state", state); +export const useWorkspaceView = (): IWorkspaceViewContext => { + const context = useContext(WorkspaceIssueViewContext); - const router = useRouter(); - const { workspaceSlug, workspaceViewId } = router.query; + if (!context) throw new Error("useWorkspaceView must be used within a WorkspaceIssueViewContext"); - const computedFilter = (filters: any) => { - const computedFilters: any = {}; - Object.keys(filters).map((key) => { - if (filters[key] != undefined) - computedFilters[key] = - typeof filters[key] === "string" || typeof filters[key] === "boolean" - ? filters[key] - : filters[key].join(","); - }); - return computedFilters; - }; - - const params: any = { - assignees: (state && state?.filters?.assignees) || undefined, - created_by: (state && state?.filters?.created_by) || undefined, - labels: (state && state?.filters?.labels) || undefined, - priority: (state && state?.filters?.priority) || undefined, - state_group: (state && state?.filters?.state_group) || undefined, - subscriber: (state && state?.filters?.subscriber) || undefined, - start_date: (state && state?.filters?.start_date) || undefined, - target_date: (state && state?.filters?.target_date) || undefined, - project: (state && state?.filters?.project) || undefined, - order_by: (state && state?.display_filters?.order_by) || "-created_at", - sub_issue: (state && state?.display_filters?.sub_issue) || false, - type: state && state?.display_filters?.type, - }; - - // current view details - const { - data: view, - error: viewError, - isLoading: viewLoading, - } = useSWR( - workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, - workspaceSlug && workspaceViewId - ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) - : null - ); - - // current view issues - const { - data: viewIssues, - mutate: mutateViewIssues, - isLoading: viewIssueLoading, - } = useSWR( - workspaceSlug && workspaceViewId - ? WORKSPACE_VIEW_ISSUES(workspaceViewId.toString(), params) - : null, - workspaceSlug && workspaceViewId - ? () => workspaceService.getViewIssues(workspaceViewId.toString(), computedFilter(params)) - : null - ); - - // console.log("----"); - // console.log("view", view); - // console.log("viewError", viewError); - // console.log("viewLoading", viewLoading); - // console.log("state", state); - - // console.log("viewIssues", viewIssues); - // console.log("viewIssueLoading", viewIssueLoading); - // console.log("mutateViewIssues", mutateViewIssues); - - // console.log("state", state); - // console.log("setFilters", setFilters); - // console.log("computedFilter(params)", computedFilter(params)); - - // console.log("----"); - - return { - view, - viewError, - viewLoading, - - viewIssues, - viewIssueLoading, - mutateViewIssues, - - state, - setFilters, - }; + return context; }; - -export default useWorkspaceIssuesView; diff --git a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx index 3dba8fbaadb..84d9db6d200 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx @@ -9,11 +9,9 @@ import useSWR from "swr"; import useProjects from "hooks/use-projects"; import useUser from "hooks/use-user"; import useWorkspaceMembers from "hooks/use-workspace-members"; -import useWorkspaceIssuesView from "hooks/use-workspace-issues-view"; - +import { useWorkspaceView } from "hooks/use-workspace-issues-view"; // context import { useProjectMyMembership } from "contexts/project-member.context"; -import { WorkspaceIssueViewContextProvider } from "contexts/workspace-view-context"; // services import projectIssuesServices from "services/issues.service"; // layouts @@ -49,9 +47,8 @@ const WorkspaceView: React.FC = () => { workspaceSlug?.toString(), Boolean(workspaceSlug) ); - const { state } = useWorkspaceIssuesView(); - - console.log("state", state); + const { filters } = useWorkspaceView(); + console.log("filters", filters); const [createViewModal, setCreateViewModal] = useState(null); @@ -169,7 +166,7 @@ const WorkspaceView: React.FC = () => { // console.log("workspaceViewIssues", workspaceViewIssues); return ( - + <> {/* @@ -300,7 +297,7 @@ const WorkspaceView: React.FC = () => {
*/} - + ); }; diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index b94e52d6bd8..6db9a14500e 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -20,6 +20,8 @@ import { SITE_TITLE } from "constants/seo-variables"; // mobx store provider import { MobxStoreProvider } from "lib/mobx/store-provider"; import MobxStoreInit from "lib/mobx/store-init"; +// context +import { WorkspaceViewProvider } from "contexts/workspace-view-context"; const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false }); @@ -36,13 +38,15 @@ function MyApp({ Component, pageProps }: AppProps) { {SITE_TITLE} - - - - - - - + + + + + + + + + ); From b8b1f4343e6ce83dcfc225696effb98a6b8454cf Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 27 Sep 2023 21:39:55 +0530 Subject: [PATCH 3/6] chore: spreadsheet view context --- .../workspace-views/workpace-view-issues.tsx | 257 +++++++++++++++ web/contexts/workspace-view-context.tsx | 41 ++- .../workspace-views/[workspaceViewId].tsx | 300 ++---------------- 3 files changed, 316 insertions(+), 282 deletions(-) create mode 100644 web/components/issues/workspace-views/workpace-view-issues.tsx diff --git a/web/components/issues/workspace-views/workpace-view-issues.tsx b/web/components/issues/workspace-views/workpace-view-issues.tsx new file mode 100644 index 00000000000..e2e52dde4a4 --- /dev/null +++ b/web/components/issues/workspace-views/workpace-view-issues.tsx @@ -0,0 +1,257 @@ +import { WORKSPACE_LABELS } from "constants/fetch-keys"; +import { useProjectMyMembership } from "contexts/project-member.context"; +import useProjects from "hooks/use-projects"; +import useUser from "hooks/use-user"; +import { useWorkspaceView } from "hooks/use-workspace-issues-view"; +import useWorkspaceMembers from "hooks/use-workspace-members"; +import { useRouter } from "next/router"; +import React, { useCallback, useState } from "react"; +import projectIssuesServices from "services/issues.service"; +import useSWR from "swr"; +import { IIssue, IWorkspaceIssueFilterOptions } from "types"; +import { CreateUpdateIssueModal } from "../modal"; +import { DeleteIssueModal } from "../delete-issue-modal"; +import { CreateUpdateViewModal } from "components/views"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { EmptyState, PrimaryButton } from "components/ui"; +import emptyView from "public/empty-state/view.svg"; +import { FiltersList, SpreadsheetView } from "components/core"; +import { STATE_GROUP } from "constants/project"; + +export const WorkspaceViewIssues = () => { + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { memberRole } = useProjectMyMembership(); + const { user } = useUser(); + const { isGuest, isViewer } = useWorkspaceMembers( + workspaceSlug?.toString(), + Boolean(workspaceSlug) + ); + const { filters, view, viewIssues, handleFilters } = useWorkspaceView(); + // console.log("filters", filters); + // console.log("view", view); + // console.log("viewIssues", viewIssues); + // console.log("handleFilters", handleFilters); + + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + + const { data: workspaceLabels } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + // const updateView = async (payload: Partial) => { + // const payloadData = { + // query_data: payload, + // }; + + // await workspaceService + // .updateView(workspaceSlug as string, workspaceViewId as string, 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 + // ); + // 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 makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + + const nullFilters = + filters.filters && + Object.keys(filters.filters).filter( + (key) => filters.filters[key as keyof IWorkspaceIssueFilterOptions] === null + ); + + const areFiltersApplied = + filters && + Object.keys(filters).length > 0 && + nullFilters.length !== Object.keys(filters).length; + + const isNotAllowed = isGuest || isViewer; + return ( + <> + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + // mutateWorkspaceIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + // mutateWorkspaceIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + // mutateWorkspaceIssues(); + }} + /> + setCreateViewModal(null)} + viewType="workspace" + preLoadedData={createViewModal} + user={user} + /> +
+
+ setCreateViewModal(true)} /> + {false ? ( + router.push(`/${workspaceSlug}/workspace-views`), + }} + /> + ) : ( +
+ {areFiltersApplied && ( + <> +
+ handleFilters("filters", updatedFilter)} + labels={workspaceLabels} + members={workspaceMembers?.map((m) => m.member)} + stateGroup={STATE_GROUP} + project={joinedProjects} + clearAllFilters={() => + handleFilters("filters", { + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + start_date: null, + target_date: null, + subscriber: null, + project: null, + }) + } + /> + { + if (workspaceViewId) { + // updateView(filters); + console.log("update"); + } else + setCreateViewModal({ + query: filters, + }); + }} + className="flex items-center gap-2 text-sm" + > + {!workspaceViewId && } + {workspaceViewId ? "Update" : "Save"} view + +
+ {
} + + )} + +
+ )} +
+
+ + ); +}; diff --git a/web/contexts/workspace-view-context.tsx b/web/contexts/workspace-view-context.tsx index 51ea22af5b7..85a8b86b539 100644 --- a/web/contexts/workspace-view-context.tsx +++ b/web/contexts/workspace-view-context.tsx @@ -88,19 +88,22 @@ export const WorkspaceViewProvider: React.FC<{ children: React.ReactNode }> = ({ return computedFilters; }; - const params: any = { - assignees: (filters && filters?.filters?.assignees) || undefined, - created_by: (filters && filters?.filters?.created_by) || undefined, - labels: (filters && filters?.filters?.labels) || undefined, - priority: (filters && filters?.filters?.priority) || undefined, - state_group: (filters && filters?.filters?.state_group) || undefined, - subscriber: (filters && filters?.filters?.subscriber) || undefined, - start_date: (filters && filters?.filters?.start_date) || undefined, - target_date: (filters && filters?.filters?.target_date) || undefined, - project: (filters && filters?.filters?.project) || undefined, - order_by: (filters && filters?.display_filters?.order_by) || "-created_at", - sub_issue: (filters && filters?.display_filters?.sub_issue) || false, - type: filters && filters?.display_filters?.type, + const computedParams = (filters: any) => { + const params: any = { + assignees: (filters && filters?.filters?.assignees) || undefined, + created_by: (filters && filters?.filters?.created_by) || undefined, + labels: (filters && filters?.filters?.labels) || undefined, + priority: (filters && filters?.filters?.priority) || undefined, + state_group: (filters && filters?.filters?.state_group) || undefined, + subscriber: (filters && filters?.filters?.subscriber) || undefined, + start_date: (filters && filters?.filters?.start_date) || undefined, + target_date: (filters && filters?.filters?.target_date) || undefined, + project: (filters && filters?.filters?.project) || undefined, + order_by: (filters && filters?.display_filters?.order_by) || "-created_at", + sub_issue: (filters && filters?.display_filters?.sub_issue) || false, + type: filters && filters?.display_filters?.type, + }; + return params; }; const { data: view, isLoading: viewLoading } = useSWR( @@ -111,15 +114,19 @@ export const WorkspaceViewProvider: React.FC<{ children: React.ReactNode }> = ({ ); const { data: viewIssues, isLoading: viewIssueLoading } = useSWR( - workspaceSlug && view && workspaceViewId - ? WORKSPACE_VIEW_ISSUES(workspaceViewId.toString(), params) + workspaceSlug && view && workspaceViewId && filters + ? WORKSPACE_VIEW_ISSUES(workspaceViewId.toString(), computedParams(filters)) : null, workspaceSlug && view && workspaceViewId - ? () => workspaceService.getViewIssues(workspaceViewId.toString(), computedFilter(params)) + ? () => + workspaceService.getViewIssues( + workspaceViewId.toString(), + computedFilter(computedParams(filters)) + ) : null ); - console.log("view", view); + // console.log("view", view); console.log("viewIssues", viewIssues); useEffect(() => { diff --git a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx index 84d9db6d200..ea67d4aa8be 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[workspaceViewId].tsx @@ -9,7 +9,6 @@ import useSWR from "swr"; import useProjects from "hooks/use-projects"; import useUser from "hooks/use-user"; import useWorkspaceMembers from "hooks/use-workspace-members"; -import { useWorkspaceView } from "hooks/use-workspace-issues-view"; // context import { useProjectMyMembership } from "contexts/project-member.context"; // services @@ -28,7 +27,6 @@ import { EmptyState, PrimaryButton } from "components/ui"; 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_LABELS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; // constant @@ -36,269 +34,41 @@ import { STATE_GROUP } from "constants/project"; // types import { IIssue, IWorkspaceIssueFilterOptions } from "types"; import workspaceService from "services/workspace.service"; - -const WorkspaceView: React.FC = () => { - const router = useRouter(); - const { workspaceSlug, workspaceViewId } = router.query; - - const { memberRole } = useProjectMyMembership(); - const { user } = useUser(); - const { isGuest, isViewer } = useWorkspaceMembers( - workspaceSlug?.toString(), - Boolean(workspaceSlug) - ); - const { filters } = useWorkspaceView(); - console.log("filters", filters); - - const [createViewModal, setCreateViewModal] = useState(null); - - // create issue modal - const [createIssueModal, setCreateIssueModal] = useState(false); - const [preloadedData, setPreloadedData] = useState< - (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined - >(undefined); - - // update issue modal - const [editIssueModal, setEditIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState< - (IIssue & { actionType: "edit" | "delete" }) | undefined - >(undefined); - - // delete issue modal - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [issueToDelete, setIssueToDelete] = useState(null); - - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - - const { data: workspaceLabels } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null - ); - - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - - // const updateView = async (payload: Partial) => { - // const payloadData = { - // query_data: payload, - // }; - - // await workspaceService - // .updateView(workspaceSlug as string, workspaceViewId as string, 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 - // ); - // 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 makeIssueCopy = useCallback( - (issue: IIssue) => { - setCreateIssueModal(true); - - setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); - }, - [setCreateIssueModal, setPreloadedData] - ); - - const handleEditIssue = useCallback( - (issue: IIssue) => { - setEditIssueModal(true); - setIssueToEdit({ - ...issue, - actionType: "edit", - cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - module: issue.issue_module ? issue.issue_module.module : null, - }); - }, - [setEditIssueModal, setIssueToEdit] - ); - - const handleDeleteIssue = useCallback( - (issue: IIssue) => { - setDeleteIssueModal(true); - setIssueToDelete(issue); - }, - [setDeleteIssueModal, setIssueToDelete] - ); - - const handleIssueAction = useCallback( - (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { - if (action === "copy") makeIssueCopy(issue); - else if (action === "edit") handleEditIssue(issue); - else if (action === "delete") handleDeleteIssue(issue); - }, - [makeIssueCopy, handleEditIssue, handleDeleteIssue] - ); - - // const nullFilters = - // filters && - // Object.keys(filters).filter( - // (key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null - // ); - - // const areFiltersApplied = - // filters && - // Object.keys(filters).length > 0 && - // nullFilters.length !== Object.keys(filters).length; - - // const isNotAllowed = isGuest || isViewer; - - // console.log("workspaceViewIssues", workspaceViewIssues); - - return ( - <> - {/* - - - {workspaceViewDetails ? `${workspaceViewDetails.name} Issues` : "Workspace Issues"} - -
- } - right={ -
- - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - -
- } - > - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => { - mutateWorkspaceIssues(); - }} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => { - mutateWorkspaceIssues(); - }} - /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateWorkspaceIssues(); +import { useWorkspaceView } from "hooks/use-workspace-issues-view"; +import { WorkspaceViewProvider } from "contexts/workspace-view-context"; +import { WorkspaceViewIssues } from "components/issues/workspace-views/workpace-view-issues"; + +const WorkspaceView: React.FC = () => ( + + + + {/* {view ? `${view.name} Issues` : "Workspace Issues"} */} + issues + +
+ } + right={ +
+ + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); }} - /> - setCreateViewModal(null)} - viewType="workspace" - preLoadedData={createViewModal} - user={user} - /> -
-
- setCreateViewModal(true)} /> - {error ? ( - router.push(`/${workspaceSlug}/workspace-views`), - }} - /> - ) : ( -
- {true && ( - <> -
- setFilters(updatedFilter)} - labels={workspaceLabels} - members={workspaceMembers?.map((m) => m.member)} - stateGroup={STATE_GROUP} - project={joinedProjects} - clearAllFilters={() => - setFilters({ - assignees: null, - created_by: null, - labels: null, - priority: null, - state_group: null, - start_date: null, - target_date: null, - subscriber: null, - project: null, - }) - } - /> - { - if (workspaceViewId) { - // updateView(filters); - console.log("update"); - } else - setCreateViewModal({ - query: filters, - }); - }} - className="flex items-center gap-2 text-sm" - > - {!workspaceViewId && } - {workspaceViewId ? "Update" : "Save"} view - -
- {
} - - )} - -
- )} -
-
- */} - - ); -}; + > + + Add Issue + +
+ } + > + + + + +); export default WorkspaceView; From 8913f65305320b192c484755b00aaa5a7defa5ec Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 28 Sep 2023 19:55:35 +0530 Subject: [PATCH 4/6] chore: display filters and properties added in workspace view and code refactor --- .../command-palette/command-pallette.tsx | 1 - web/components/core/filters/filters-list.tsx | 61 +-- web/components/core/filters/index.ts | 1 + .../core/filters/workspace-filters-list.tsx | 366 ++++++++++++++++++ web/components/core/views/issues-view.tsx | 1 - .../issue-column/issue-column.tsx | 6 +- .../spreadsheet-view/spreadsheet-view.tsx | 26 +- .../state-column/state-column.tsx | 2 +- .../my-issues/my-issues-select-filters.tsx | 125 +----- .../workspace-views/workpace-view-issues.tsx | 106 ++--- .../workspace-views/workspace-all-issue.tsx | 234 +++++++++++ .../workspace-assigned-issue.tsx | 155 ++++++++ .../workspace-created-issues.tsx | 147 +++++++ .../workspace-issue-view-option.tsx | 52 ++- .../workspace-subscribed-issue.tsx | 148 +++++++ .../profile/profile-issues-view-options.tsx | 81 ++-- web/components/views/delete-view-modal.tsx | 81 ++-- web/components/views/form.tsx | 33 +- web/components/views/modal.tsx | 119 ++---- web/components/views/select-filters.tsx | 262 +------------ web/components/views/single-view-item.tsx | 3 +- .../views/delete-workspace-view-modal.tsx | 141 +++++++ web/components/workspace/views/form.tsx | 213 ++++++++++ .../workspace/views/global-select-filters.tsx | 301 ++++++++++++++ web/components/workspace/views/modal.tsx | 155 ++++++++ web/constants/fetch-keys.ts | 6 +- web/contexts/profile-issues-context.tsx | 1 + web/contexts/workspace-view-context.tsx | 104 +++-- web/hooks/my-issues/use-my-issues-filter.tsx | 2 +- ...issues-view.tsx => use-workspace-view.tsx} | 0 web/hooks/use-worskpace-issue-filter.tsx | 126 ------ .../project-authorization-wrapper.tsx | 5 +- .../workspace-authorization-wrapper.tsx | 81 ++-- .../projects/[projectId]/views/index.tsx | 2 - .../workspace-views/[workspaceViewId].tsx | 44 +-- .../workspace-views/all-issues.tsx | 306 ++------------- .../workspace-views/assigned.tsx | 221 ++--------- .../workspace-views/created.tsx | 221 ++--------- .../[workspaceSlug]/workspace-views/index.tsx | 26 +- .../workspace-views/subscribed.tsx | 221 ++--------- web/pages/_app.tsx | 18 +- web/services/workspace.service.ts | 4 +- web/types/view-props.d.ts | 29 +- web/types/views.d.ts | 12 +- web/types/workspace-views.d.ts | 6 +- 45 files changed, 2342 insertions(+), 1913 deletions(-) create mode 100644 web/components/core/filters/workspace-filters-list.tsx create mode 100644 web/components/issues/workspace-views/workspace-all-issue.tsx create mode 100644 web/components/issues/workspace-views/workspace-assigned-issue.tsx create mode 100644 web/components/issues/workspace-views/workspace-created-issues.tsx create mode 100644 web/components/issues/workspace-views/workspace-subscribed-issue.tsx create mode 100644 web/components/workspace/views/delete-workspace-view-modal.tsx create mode 100644 web/components/workspace/views/form.tsx create mode 100644 web/components/workspace/views/global-select-filters.tsx create mode 100644 web/components/workspace/views/modal.tsx rename web/hooks/{use-workspace-issues-view.tsx => use-workspace-view.tsx} (100%) delete mode 100644 web/hooks/use-worskpace-issue-filter.tsx diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index 8e17cbafde0..f183de9c6c1 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -161,7 +161,6 @@ export const CommandPalette: React.FC = observer(() => { /> setIsCreateViewModalOpen(false)} - viewType="project" isOpen={isCreateViewModalOpen} user={user} /> diff --git a/web/components/core/filters/filters-list.tsx b/web/components/core/filters/filters-list.tsx index a2b12b69c8a..81a12bd862d 100644 --- a/web/components/core/filters/filters-list.tsx +++ b/web/components/core/filters/filters-list.tsx @@ -10,14 +10,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // helpers import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; // types -import { - IIssueFilterOptions, - IIssueLabels, - IProject, - IState, - IUserLite, - TStateGroups, -} from "types"; +import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types"; // constants import { STATE_GROUP_COLORS } from "constants/state"; @@ -27,9 +20,7 @@ type Props = { clearAllFilters: (...args: any) => void; labels: IIssueLabels[] | undefined; members: IUserLite[] | undefined; - states?: IState[] | undefined; - stateGroup?: string[] | undefined; - project?: IProject[] | undefined; + states: IState[] | undefined; }; export const FiltersList: React.FC = ({ @@ -39,7 +30,6 @@ export const FiltersList: React.FC = ({ labels, members, states, - project, }) => { if (!filters) return <>; @@ -165,29 +155,6 @@ export const FiltersList: React.FC = ({ : key === "assignees" ? filters.assignees?.map((memberId: string) => { const member = members?.find((m) => m.id === memberId); - return ( -
- - {member?.display_name} - - setFilters({ - assignees: filters.assignees?.filter((p: any) => p !== memberId), - }) - } - > - - -
- ); - }) - : key === "subscriber" - ? filters.subscriber?.map((memberId: string) => { - const member = members?.find((m) => m.id === memberId); return (
= ({
); }) - : key === "project" - ? filters.project?.map((projectId) => { - const currentProject = project?.find((p) => p.id === projectId); - console.log("currentProject", currentProject); - console.log("currentProject", projectId); - return ( -

- {currentProject?.name} - - setFilters({ - project: filters.project?.filter((p) => p !== projectId), - }) - } - > - - -

- ); - }) : (filters[key] as any)?.join(", ")} +
+
+ ) : ( +
+ {filters[key as keyof typeof filters]} + +
+ )} +
+ ); + })} + {Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && ( + + )} +
+ ); +}; diff --git a/web/components/core/views/issues-view.tsx b/web/components/core/views/issues-view.tsx index c09e7c80b20..9a2d482faa2 100644 --- a/web/components/core/views/issues-view.tsx +++ b/web/components/core/views/issues-view.tsx @@ -481,7 +481,6 @@ export const IssuesView: React.FC = ({ setCreateViewModal(null)} - viewType="project" preLoadedData={createViewModal} user={user} /> diff --git a/web/components/core/views/spreadsheet-view/issue-column/issue-column.tsx b/web/components/core/views/spreadsheet-view/issue-column/issue-column.tsx index c1df89c9246..1afcda5e1af 100644 --- a/web/components/core/views/spreadsheet-view/issue-column/issue-column.tsx +++ b/web/components/core/views/spreadsheet-view/issue-column/issue-column.tsx @@ -82,10 +82,10 @@ export const IssueColumn: React.FC = ({ const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( -
+
{properties.key && ( diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index e8cbf7f6d35..7da4c9639ec 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -24,6 +24,7 @@ import { IssuePeekOverview } from "components/issues"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; import useLocalStorage from "hooks/use-local-storage"; +import { useWorkspaceView } from "hooks/use-workspace-view"; // types import { ICurrentUserResponse, @@ -32,7 +33,6 @@ import { TIssueOrderByOptions, UserAuth, } from "types"; -import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; import { CYCLE_DETAILS, CYCLE_ISSUES_WITH_PARAMS, @@ -47,7 +47,6 @@ import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import projectIssuesServices from "services/issues.service"; // icon import { CheckIcon, ChevronDownIcon, PlusIcon } from "lucide-react"; -import useWorkspaceIssuesView from "hooks/use-workspace-issues-view"; type Props = { spreadsheetIssues: IIssue[]; @@ -130,16 +129,7 @@ export const SpreadsheetView: React.FC = ({ router.pathname.includes(path.path) ); - // const { params: workspaceViewParams } = useWorkspaceIssuesFilters( - // workspaceSlug?.toString(), - // workspaceViewId?.toString() - // ); - - const { - params: workspaceViewParams, - displayFilters: workspaceDisplayFilters, - setDisplayFilters: setWorkspaceDisplayFilters, - } = useWorkspaceIssuesView(); + const { params: workspaceViewParams, handleFilters } = useWorkspaceView(); const { params, displayFilters, setDisplayFilters } = useSpreadsheetIssuesView(); @@ -224,8 +214,8 @@ export const SpreadsheetView: React.FC = ({ moduleId, viewId, workspaceViewId, - currentWorkspaceIssuePath, workspaceViewParams, + currentWorkspaceIssuePath, params, user, ] @@ -235,7 +225,7 @@ export const SpreadsheetView: React.FC = ({ const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => { if (!workspaceViewId || !currentWorkspaceIssuePath) - setWorkspaceDisplayFilters({ order_by: order }); + handleFilters("display_filters", { order_by: order }); else setDisplayFilters({ order_by: order }); setSelectedMenuItem(`${order}_${itemKey}`); setActiveSortingProperty(order === "-created_at" ? "" : itemKey); @@ -248,7 +238,7 @@ export const SpreadsheetView: React.FC = ({ ascendingOrder: TIssueOrderByOptions, descendingOrder: TIssueOrderByOptions ) => ( -
+
= ({ workspaceSlug={workspaceSlug?.toString() ?? ""} readOnly={disableUserActions} /> -
+
{spreadsheetIssues ? ( <>
- + ID diff --git a/web/components/core/views/spreadsheet-view/state-column/state-column.tsx b/web/components/core/views/spreadsheet-view/state-column/state-column.tsx index 6b3d3c69694..7db4b7d206a 100644 --- a/web/components/core/views/spreadsheet-view/state-column/state-column.tsx +++ b/web/components/core/views/spreadsheet-view/state-column/state-column.tsx @@ -76,7 +76,7 @@ export const StateColumn: React.FC = ({ value={issue.state_detail} projectId={projectId} onChange={handleStateChange} - buttonClassName="!p-0 !rounded-none !shadow-none !border-0" + buttonClassName="!shadow-none !border-0" hideDropdownArrow disabled={isNotAllowed} /> diff --git a/web/components/issues/my-issues/my-issues-select-filters.tsx b/web/components/issues/my-issues/my-issues-select-filters.tsx index 8085b5e7851..ce8e03797f2 100644 --- a/web/components/issues/my-issues/my-issues-select-filters.tsx +++ b/web/components/issues/my-issues/my-issues-select-filters.tsx @@ -4,21 +4,18 @@ import { useRouter } from "next/router"; import useSWR from "swr"; -// hook -import useProjects from "hooks/use-projects"; -import useWorkspaceMembers from "hooks/use-workspace-members"; // services import issuesService from "services/issues.service"; // components import { DateFilterModal } from "components/core"; // ui -import { Avatar, MultiLevelDropdown } from "components/ui"; +import { MultiLevelDropdown } from "components/ui"; // icons import { PriorityIcon, StateGroupIcon } from "components/icons"; // helpers import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types -import { IIssueFilterOptions, TStateGroups } from "types"; +import { IIssueFilterOptions, IQuery, TStateGroups } from "types"; // fetch-keys import { WORKSPACE_LABELS } from "constants/fetch-keys"; // constants @@ -26,7 +23,7 @@ import { GROUP_CHOICES, PRIORITIES } from "constants/project"; import { DATE_FILTER_OPTIONS } from "constants/filters"; type Props = { - filters: Partial; + filters: Partial | IQuery; onSelect: (option: any) => void; direction?: "left" | "right"; height?: "sm" | "md" | "rg" | "lg"; @@ -58,11 +55,6 @@ export const MyIssuesSelectFilters: React.FC = ({ : null ); - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - return ( <> {isDateFilterModalOpen && ( @@ -82,19 +74,25 @@ export const MyIssuesSelectFilters: React.FC = ({ height={height} options={[ { - id: "project", - label: "Project", - value: joinedProjects, + id: "priority", + label: "Priority", + value: PRIORITIES, hasChildren: true, - children: joinedProjects?.map((project) => ({ - id: project.id, - label:
{project.name}
, - value: { - key: "project", - value: project.id, - }, - selected: filters?.project?.includes(project.id), - })), + children: [ + ...PRIORITIES.map((priority) => ({ + id: priority === null ? "null" : priority, + label: ( +
+ {priority ?? "None"} +
+ ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), + ], }, { id: "state_group", @@ -144,87 +142,6 @@ export const MyIssuesSelectFilters: React.FC = ({ selected: filters?.labels?.includes(label.id), })), }, - { - id: "priority", - label: "Priority", - value: PRIORITIES, - hasChildren: true, - children: [ - ...PRIORITIES.map((priority) => ({ - id: priority === null ? "null" : priority, - label: ( -
- {priority ?? "None"} -
- ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - ], - }, - { - id: "created_by", - label: "Created by", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "created_by", - value: member.member.id, - }, - selected: filters?.created_by?.includes(member.member.id), - })), - }, - { - id: "assignees", - label: "Assignees", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "assignees", - value: member.member.id, - }, - selected: filters?.assignees?.includes(member.member.id), - })), - }, - { - id: "subscriber", - label: "Subscriber", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "subscriber", - value: member.member.id, - }, - selected: filters?.subscriber?.includes(member.member.id), - })), - }, { id: "start_date", label: "Start date", diff --git a/web/components/issues/workspace-views/workpace-view-issues.tsx b/web/components/issues/workspace-views/workpace-view-issues.tsx index e2e52dde4a4..708e0eb9f93 100644 --- a/web/components/issues/workspace-views/workpace-view-issues.tsx +++ b/web/components/issues/workspace-views/workpace-view-issues.tsx @@ -1,22 +1,33 @@ -import { WORKSPACE_LABELS } from "constants/fetch-keys"; +import React, { useCallback, useState } from "react"; + +import useSWR from "swr"; + +import { useRouter } from "next/router"; + +// context import { useProjectMyMembership } from "contexts/project-member.context"; +// service +import projectIssuesServices from "services/issues.service"; +// hooks import useProjects from "hooks/use-projects"; import useUser from "hooks/use-user"; -import { useWorkspaceView } from "hooks/use-workspace-issues-view"; +import { useWorkspaceView } from "hooks/use-workspace-view"; import useWorkspaceMembers from "hooks/use-workspace-members"; -import { useRouter } from "next/router"; -import React, { useCallback, useState } from "react"; -import projectIssuesServices from "services/issues.service"; -import useSWR from "swr"; -import { IIssue, IWorkspaceIssueFilterOptions } from "types"; -import { CreateUpdateIssueModal } from "../modal"; -import { DeleteIssueModal } from "../delete-issue-modal"; -import { CreateUpdateViewModal } from "components/views"; +// components import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; import { EmptyState, PrimaryButton } from "components/ui"; +import { SpreadsheetView, WorkspaceFiltersList } from "components/core"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; +// icon +import { PlusIcon } from "components/icons"; +// image import emptyView from "public/empty-state/view.svg"; -import { FiltersList, SpreadsheetView } from "components/core"; +// constants +import { WORKSPACE_LABELS } from "constants/fetch-keys"; import { STATE_GROUP } from "constants/project"; +// types +import { IIssue, IWorkspaceIssueFilterOptions } from "types"; export const WorkspaceViewIssues = () => { const router = useRouter(); @@ -28,11 +39,7 @@ export const WorkspaceViewIssues = () => { workspaceSlug?.toString(), Boolean(workspaceSlug) ); - const { filters, view, viewIssues, handleFilters } = useWorkspaceView(); - // console.log("filters", filters); - // console.log("view", view); - // console.log("viewIssues", viewIssues); - // console.log("handleFilters", handleFilters); + const { filters, viewIssues, mutateViewIssues, handleFilters } = useWorkspaceView(); const [createViewModal, setCreateViewModal] = useState(null); @@ -62,39 +69,6 @@ export const WorkspaceViewIssues = () => { const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - // const updateView = async (payload: Partial) => { - // const payloadData = { - // query_data: payload, - // }; - - // await workspaceService - // .updateView(workspaceSlug as string, workspaceViewId as string, 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 - // ); - // 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 makeIssueCopy = useCallback( (issue: IIssue) => { setCreateIssueModal(true); @@ -141,9 +115,9 @@ export const WorkspaceViewIssues = () => { ); const areFiltersApplied = - filters && - Object.keys(filters).length > 0 && - nullFilters.length !== Object.keys(filters).length; + filters.filters && + Object.keys(filters.filters).length > 0 && + nullFilters.length !== Object.keys(filters.filters).length; const isNotAllowed = isGuest || isViewer; return ( @@ -154,33 +128,25 @@ export const WorkspaceViewIssues = () => { prePopulateData={{ ...preloadedData, }} - onSubmit={async () => { - // mutateWorkspaceIssues(); - }} + onSubmit={async () => mutateViewIssues()} /> setEditIssueModal(false)} data={issueToEdit} - onSubmit={async () => { - // mutateWorkspaceIssues(); - }} + onSubmit={async () => mutateViewIssues()} /> setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueToDelete} user={user} - onSubmit={async () => { - // mutateWorkspaceIssues(); - }} + onSubmit={async () => mutateViewIssues()} /> - setCreateViewModal(null)} - viewType="workspace" preLoadedData={createViewModal} - user={user} />
@@ -200,7 +166,7 @@ export const WorkspaceViewIssues = () => { {areFiltersApplied && ( <>
- handleFilters("filters", updatedFilter)} labels={workspaceLabels} @@ -223,12 +189,10 @@ export const WorkspaceViewIssues = () => { /> { - if (workspaceViewId) { - // updateView(filters); - console.log("update"); - } else + if (workspaceViewId) handleFilters("filters", filters.filters, true); + else setCreateViewModal({ - query: filters, + query: filters.filters, }); }} className="flex items-center gap-2 text-sm" @@ -242,7 +206,7 @@ export const WorkspaceViewIssues = () => { )} { + const router = useRouter(); + const { workspaceSlug, workspaceViewId } = router.query; + + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const { user } = useUser(); + const { memberRole } = useProjectMyMembership(); + + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + const { data: workspaceLabels } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + const { filters, handleFilters } = useWorkspaceView(); + + const params: any = { + assignees: filters?.filters?.assignees ? filters?.filters?.assignees.join(",") : undefined, + subscriber: filters?.filters?.subscriber ? filters?.filters?.subscriber.join(",") : undefined, + state_group: filters?.filters?.state_group + ? filters?.filters?.state_group.join(",") + : undefined, + priority: filters?.filters?.priority ? filters?.filters?.priority.join(",") : undefined, + labels: filters?.filters?.labels ? filters?.filters?.labels.join(",") : undefined, + created_by: filters?.filters?.created_by ? filters?.filters?.created_by.join(",") : undefined, + start_date: filters?.filters?.start_date ? filters?.filters?.start_date.join(",") : undefined, + target_date: filters?.filters?.target_date + ? filters?.filters?.target_date.join(",") + : undefined, + project: filters?.filters?.project ? filters?.filters?.project.join(",") : undefined, + sub_issue: false, + type: undefined, + }; + + const { data: viewIssues, mutate: mutateViewIssues } = useSWR( + workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null + ); + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + + const nullFilters = + filters.filters && + Object.keys(filters.filters).filter( + (key) => filters.filters[key as keyof IWorkspaceIssueFilterOptions] === null + ); + + const areFiltersApplied = + filters.filters && + Object.keys(filters.filters).length > 0 && + nullFilters.length !== Object.keys(filters.filters).length; + + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + + return ( + <> + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateViewIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateViewIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateViewIssues(); + }} + /> + setCreateViewModal(null)} + preLoadedData={createViewModal} + /> +
+
+ setCreateViewModal(true)} /> +
+ {areFiltersApplied && ( + <> +
+ handleFilters("filters", updatedFilter)} + labels={workspaceLabels} + members={workspaceMembers?.map((m) => m.member)} + stateGroup={STATE_GROUP} + project={joinedProjects} + clearAllFilters={() => + handleFilters("filters", { + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + start_date: null, + target_date: null, + subscriber: null, + project: null, + }) + } + /> + { + if (workspaceViewId) handleFilters("filters", filters.filters, true); + else + setCreateViewModal({ + query: filters.filters, + }); + }} + className="flex items-center gap-2 text-sm" + > + {!workspaceViewId && } + {workspaceViewId ? "Update" : "Save"} view + +
+ {
} + + )} + +
+
+
+ + ); +}; diff --git a/web/components/issues/workspace-views/workspace-assigned-issue.tsx b/web/components/issues/workspace-views/workspace-assigned-issue.tsx new file mode 100644 index 00000000000..ccee9ab8481 --- /dev/null +++ b/web/components/issues/workspace-views/workspace-assigned-issue.tsx @@ -0,0 +1,155 @@ +import React, { useCallback, useState } from "react"; +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// hook +import useUser from "hooks/use-user"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +// components +import { SpreadsheetView } from "components/core"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; +// fetch-keys +import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; +// types +import { IIssue } from "types"; + +export const WorkspaceAssignedIssue = () => { + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { user } = useUser(); + + const { memberRole } = useProjectMyMembership(); + + const params: any = { + assignees: user?.id ?? undefined, + sub_issue: false, + }; + + // const { data: viewDetails, error } = useSWR( + // workspaceSlug && workspaceViewId ? WORKSPACE_VIEW_DETAILS(workspaceViewId.toString()) : null, + // workspaceSlug && workspaceViewId + // ? () => workspaceService.getViewDetails(workspaceSlug.toString(), workspaceViewId.toString()) + // : null + // ); + + const { data: viewIssues, mutate: mutateIssues } = useSWR( + workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null + ); + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + return ( + <> + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateIssues(); + }} + /> + setCreateViewModal(null)} + preLoadedData={createViewModal} + /> +
+
+ setCreateViewModal(true)} /> + +
+ +
+
+
+ + ); +}; diff --git a/web/components/issues/workspace-views/workspace-created-issues.tsx b/web/components/issues/workspace-views/workspace-created-issues.tsx new file mode 100644 index 00000000000..bcc83c38b4b --- /dev/null +++ b/web/components/issues/workspace-views/workspace-created-issues.tsx @@ -0,0 +1,147 @@ +import React, { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// hook +import useUser from "hooks/use-user"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +// components +import { SpreadsheetView } from "components/core"; +import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; +// fetch-keys +import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; +// types +import { IIssue } from "types"; + +export const WorkspaceCreatedIssues = () => { + const [createViewModal, setCreateViewModal] = useState(null); + + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // update issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { user } = useUser(); + const { memberRole } = useProjectMyMembership(); + + const params: any = { + created_by: user?.id ?? undefined, + sub_issue: false, + }; + + const { data: viewIssues, mutate: mutateIssues } = useSWR( + workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null + ); + + const makeIssueCopy = useCallback( + (issue: IIssue) => { + setCreateIssueModal(true); + + setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }, + [setEditIssueModal, setIssueToEdit] + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + return ( + <> + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + onSubmit={async () => { + mutateIssues(); + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + onSubmit={async () => { + mutateIssues(); + }} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + onSubmit={async () => { + mutateIssues(); + }} + /> + setCreateViewModal(null)} + preLoadedData={createViewModal} + /> +
+
+ setCreateViewModal(true)} /> +
+ +
+
+
+ + ); +}; diff --git a/web/components/issues/workspace-views/workspace-issue-view-option.tsx b/web/components/issues/workspace-views/workspace-issue-view-option.tsx index adfff64ac08..b635ff870ca 100644 --- a/web/components/issues/workspace-views/workspace-issue-view-option.tsx +++ b/web/components/issues/workspace-views/workspace-issue-view-option.tsx @@ -3,10 +3,9 @@ import React from "react"; import { useRouter } from "next/router"; // hooks -import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; -import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; +import { useWorkspaceView } from "hooks/use-workspace-view"; // components -import { MyIssuesSelectFilters } from "components/issues"; +import { GlobalSelectFilters } from "components/workspace/views/global-select-filters"; // ui import { Tooltip } from "components/ui"; // icons @@ -17,7 +16,6 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types import { TIssueViewOptions } from "types"; -import useWorkspaceIssuesView from "hooks/use-workspace-issues-view"; const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ { @@ -34,14 +32,7 @@ export const WorkspaceIssuesViewOptions: React.FC = () => { const router = useRouter(); const { workspaceSlug, workspaceViewId } = router.query; - // const { displayFilters, setDisplayFilters } = useMyIssuesFilters(workspaceSlug?.toString()); - - // const { filters, setFilters } = useWorkspaceIssuesFilters( - // workspaceSlug?.toString(), - // workspaceViewId?.toString() - // ); - - const { filters, setFilters, displayFilters, setDisplayFilters } = useWorkspaceIssuesView(); + const { filters, handleFilters } = useWorkspaceView(); const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues"); @@ -61,12 +52,12 @@ export const WorkspaceIssuesViewOptions: React.FC = () => { - ); - })} + return ( + + ); + })}
diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index 0d49c62cce7..61c627430f0 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -8,7 +8,6 @@ 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 @@ -18,17 +17,16 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types import type { ICurrentUserResponse, IView } from "types"; // fetch-keys -import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; +import { 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, viewType, user }) => { +export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -43,64 +41,33 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, view const handleDeletion = async () => { setIsDeleteLoading(true); + if (!workspaceSlug || !data || !projectId) return; - if (viewType === "project") { - 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) + ); - 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) - ); + handleClose(); - 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); + setToastAlert({ + type: "success", + title: "Success!", + message: "View deleted successfully.", }); - } 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); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be deleted. Please try again.", }); - } + }) + .finally(() => { + setIsDeleteLoading(false); + }); }; return ( diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 29d16ca9c52..0c57a954233 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -10,8 +10,6 @@ import { useForm } from "react-hook-form"; import stateService from "services/state.service"; // hooks import useProjectMembers from "hooks/use-project-members"; -import useProjects from "hooks/use-projects"; -import useWorkspaceMembers from "hooks/use-workspace-members"; // components import { FiltersList } from "components/core"; import { SelectFilters } from "components/views"; @@ -24,14 +22,13 @@ import { getStatesList } from "helpers/state.helper"; import { IQuery, IView } from "types"; import issuesService from "services/issues.service"; // fetch-keys -import { PROJECT_ISSUE_LABELS, STATES_LIST, WORKSPACE_LABELS } from "constants/fetch-keys"; +import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys"; type Props = { handleFormSubmit: (values: IView) => Promise; handleClose: () => void; status: boolean; data?: IView | null; - viewType?: "workspace" | "project"; preLoadedData?: Partial | null; }; @@ -45,7 +42,6 @@ export const ViewForm: React.FC = ({ handleClose, status, data, - viewType, preLoadedData, }) => { const router = useRouter(); @@ -81,26 +77,8 @@ export const ViewForm: React.FC = ({ ? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString()) : null ); - - const { data: workspaceLabels } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null - ); - - const labelOptions = viewType === "workspace" ? workspaceLabels : labels; - const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString()); - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - - const memberOptions = - viewType === "workspace" - ? workspaceMembers?.map((m) => m.member) - : members?.map((m) => m.member); - - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - const handleCreateUpdateView = async (formData: IView) => { await handleFormSubmit(formData); @@ -113,14 +91,12 @@ export const ViewForm: React.FC = ({ setValue("query", { assignees: null, created_by: null, - subscriber: null, labels: null, priority: null, state: null, - state_group: null, start_date: null, target_date: null, - project: null, + type: null, }); }; @@ -209,10 +185,9 @@ export const ViewForm: React.FC = ({
m.member)} states={states} - project={joinedProjects} clearAllFilters={clearAllFilters} setFilters={(query: any) => { setValue("query", { diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index 93fa72321e1..c1ff54231b6 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -8,7 +8,6 @@ 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 @@ -16,12 +15,10 @@ import { ViewForm } from "components/views"; // types import { ICurrentUserResponse, IView } from "types"; // fetch-keys -import { VIEWS_LIST, WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; -import { IWorkspaceView } from "types/workspace-views"; +import { VIEWS_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; - viewType: "project" | "workspace"; handleClose: () => void; data?: IView | null; preLoadedData?: Partial | null; @@ -30,7 +27,6 @@ type Props = { export const CreateUpdateViewModal: React.FC = ({ isOpen, - viewType, handleClose, data, preLoadedData, @@ -50,7 +46,6 @@ export const CreateUpdateViewModal: React.FC = ({ ...payload, query_data: payload.query, }; - await viewsService .createView(workspaceSlug as string, projectId as string, payload, user) .then(() => { @@ -71,112 +66,47 @@ export const CreateUpdateViewModal: React.FC = ({ }); }); }; - const createWorkspaceView = async (payload: IWorkspaceView) => { - payload = { + + const updateView = async (payload: IView) => { + const payloadData = { ...payload, query_data: payload.query, }; - - await workspaceService - .createView(workspaceSlug as string, payload) - .then(() => { - mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string)); - handleClose(); + 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(); setToastAlert({ type: "success", title: "Success!", - message: "View created successfully.", + message: "View updated successfully.", }); }) .catch(() => { setToastAlert({ type: "error", title: "Error!", - message: "View could not be created. Please try again.", + message: "View could not be updated. Please try again.", }); }); }; - const updateView = async (payload: IView) => { - const payloadData = { - ...payload, - query_data: payload.query, - }; - 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(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "View updated successfully.", - }); - }) - .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: any) => { - if (!workspaceSlug) return; - - if (!data) { - if (viewType === "project") await createView(formData); - else createWorkspaceView(formData); - } - // else await updateView(formData); - // } else if { - // if (!workspaceSlug) return; + const handleFormSubmit = async (formData: IView) => { + if (!workspaceSlug || !projectId) return; - // if (!data) await createView(formData); - // else await updateView(formData); + if (!data) await createView(formData); + else await updateView(formData); }; return ( @@ -211,7 +141,6 @@ export const CreateUpdateViewModal: React.FC = ({ handleClose={handleClose} status={data ? true : false} data={data} - viewType={viewType} preLoadedData={preLoadedData} /> diff --git a/web/components/views/select-filters.tsx b/web/components/views/select-filters.tsx index 7b1324f5f62..c3aadc33d9f 100644 --- a/web/components/views/select-filters.tsx +++ b/web/components/views/select-filters.tsx @@ -4,9 +4,6 @@ import { useRouter } from "next/router"; import useSWR from "swr"; -// hook -import useProjects from "hooks/use-projects"; -import useWorkspaceMembers from "hooks/use-workspace-members"; // services import stateService from "services/state.service"; import projectService from "services/project.service"; @@ -21,16 +18,11 @@ import { PriorityIcon, StateGroupIcon } from "components/icons"; import { getStatesList } from "helpers/state.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types -import { IIssueFilterOptions, TStateGroups } from "types"; +import { IIssueFilterOptions } from "types"; // fetch-keys -import { - PROJECT_ISSUE_LABELS, - PROJECT_MEMBERS, - STATES_LIST, - WORKSPACE_LABELS, -} from "constants/fetch-keys"; +import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; // constants -import { GROUP_CHOICES, PRIORITIES } from "constants/project"; +import { PRIORITIES } from "constants/project"; import { DATE_FILTER_OPTIONS } from "constants/filters"; type Props = { @@ -56,7 +48,7 @@ export const SelectFilters: React.FC = ({ }); const router = useRouter(); - const { workspaceSlug, projectId, workspaceViewId } = router.query; + const { workspaceSlug, projectId } = router.query; const { data: states } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, @@ -66,20 +58,6 @@ export const SelectFilters: React.FC = ({ ); const statesList = getStatesList(states); - const workspaceViewPathName = [ - "workspace-views", - "workspace-views/all-issues", - "workspace-views/assigned", - "workspace-views/created", - "workspace-views/subscribed", - ]; - - const isWorkspaceViewPath = workspaceViewPathName.some((pathname) => - router.pathname.includes(pathname) - ); - - const isWorkspaceView = isWorkspaceViewPath || workspaceViewId; - const { data: members } = useSWR( projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId @@ -87,8 +65,6 @@ export const SelectFilters: React.FC = ({ : null ); - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - const { data: issueLabels } = useSWR( projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, workspaceSlug && projectId @@ -96,14 +72,6 @@ export const SelectFilters: React.FC = ({ : null ); - const { data: workspaceLabels } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null - ); - - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - const projectFilterOption = [ { id: "priority", @@ -283,226 +251,6 @@ export const SelectFilters: React.FC = ({ ], }, ]; - - const workspaceFilterOption = [ - { - id: "project", - label: "Project", - value: joinedProjects, - hasChildren: true, - children: joinedProjects?.map((project) => ({ - id: project.id, - label:
{project.name}
, - value: { - key: "project", - value: project.id, - }, - selected: filters?.project?.includes(project.id), - })), - }, - { - id: "state_group", - label: "State groups", - value: GROUP_CHOICES, - hasChildren: true, - children: [ - ...Object.keys(GROUP_CHOICES).map((key) => ({ - id: key, - label: ( -
- - {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} -
- ), - value: { - key: "state_group", - value: key, - }, - selected: filters?.state?.includes(key), - })), - ], - }, - { - id: "labels", - label: "Labels", - value: workspaceLabels, - hasChildren: true, - children: workspaceLabels?.map((label) => ({ - id: label.id, - label: ( -
-
- {label.name} -
- ), - value: { - key: "labels", - value: label.id, - }, - selected: filters?.labels?.includes(label.id), - })), - }, - { - id: "priority", - label: "Priority", - value: PRIORITIES, - hasChildren: true, - children: PRIORITIES.map((priority) => ({ - id: priority === null ? "null" : priority, - label: ( -
- - {priority ?? "None"} -
- ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - }, - { - id: "created_by", - label: "Created by", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "created_by", - value: member.member.id, - }, - selected: filters?.created_by?.includes(member.member.id), - })), - }, - { - id: "assignees", - label: "Assignees", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "assignees", - value: member.member.id, - }, - selected: filters?.assignees?.includes(member.member.id), - })), - }, - { - id: "subscriber", - label: "Subscriber", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "subscriber", - value: member.member.id, - }, - selected: filters?.subscriber?.includes(member.member.id), - })), - }, - { - id: "start_date", - label: "Start date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...DATE_FILTER_OPTIONS.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "start_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value), - })), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - { - id: "target_date", - label: "Due date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...DATE_FILTER_OPTIONS.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "target_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), - })), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - ]; - - const filterOption = isWorkspaceView ? workspaceFilterOption : projectFilterOption; - return ( <> {isDateFilterModalOpen && ( @@ -520,7 +268,7 @@ export const SelectFilters: React.FC = ({ onSelect={onSelect} direction={direction} height={height} - options={filterOption} + options={projectFilterOption} /> ); diff --git a/web/components/views/single-view-item.tsx b/web/components/views/single-view-item.tsx index d27eb3cf198..a2e0d213adb 100644 --- a/web/components/views/single-view-item.tsx +++ b/web/components/views/single-view-item.tsx @@ -12,6 +12,7 @@ import { CustomMenu } from "components/ui"; import viewsService from "services/views.service"; // types import { IView } from "types"; +import { IWorkspaceView } from "types/workspace-views"; // fetch keys import { VIEWS_LIST } from "constants/fetch-keys"; // hooks @@ -20,7 +21,7 @@ import useToast from "hooks/use-toast"; import { truncateText } from "helpers/string.helper"; type Props = { - view: IView; + view: IView | IWorkspaceView; viewType: "project" | "workspace"; handleEditView: () => void; handleDeleteView: () => void; diff --git a/web/components/workspace/views/delete-workspace-view-modal.tsx b/web/components/workspace/views/delete-workspace-view-modal.tsx new file mode 100644 index 00000000000..6030f630f11 --- /dev/null +++ b/web/components/workspace/views/delete-workspace-view-modal.tsx @@ -0,0 +1,141 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// services +import workspaceService from "services/workspace.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { DangerButton, SecondaryButton } from "components/ui"; +// icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +// types +import { IWorkspaceView } from "types/workspace-views"; +// fetch-keys +import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; + data: IWorkspaceView | null; +}; + +export const DeleteWorkspaceViewModal: React.FC = ({ isOpen, data, setIsOpen }) => { + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { setToastAlert } = useToast(); + + const handleClose = () => { + setIsOpen(false); + setIsDeleteLoading(false); + }; + + const handleDeletion = async () => { + setIsDeleteLoading(true); + + 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); + }); + }; + + return ( + + + +
+ + +
+
+ + +
+
+
+
+
+ + Delete View + +
+

+ Are you sure you want to delete view-{" "} + + {data?.name} + + ? All of the data related to the view will be permanently removed. This + action cannot be undone. +

+
+
+
+
+
+ Cancel + + {isDeleteLoading ? "Deleting..." : "Delete"} + +
+
+
+
+
+
+
+ ); +}; diff --git a/web/components/workspace/views/form.tsx b/web/components/workspace/views/form.tsx new file mode 100644 index 00000000000..b16d613990c --- /dev/null +++ b/web/components/workspace/views/form.tsx @@ -0,0 +1,213 @@ +import { useEffect } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// react-hook-form +import { useForm } from "react-hook-form"; +// services +import issuesService from "services/issues.service"; + +// hooks +import useProjects from "hooks/use-projects"; +import useWorkspaceMembers from "hooks/use-workspace-members"; +// components +import { WorkspaceFiltersList } from "components/core"; +import { GlobalSelectFilters } from "components/workspace/views/global-select-filters"; + +// ui +import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; +// helpers +import { checkIfArraysHaveSameElements } from "helpers/array.helper"; +// types +import { IQuery } from "types"; +import { IWorkspaceView } from "types/workspace-views"; +// fetch-keys +import { WORKSPACE_LABELS } from "constants/fetch-keys"; +import { STATE_GROUP } from "constants/project"; + +type Props = { + handleFormSubmit: (values: IWorkspaceView) => Promise; + handleClose: () => void; + status: boolean; + data?: IWorkspaceView | null; + preLoadedData?: Partial | null; +}; + +const defaultValues: Partial = { + name: "", + description: "", +}; + +export const WorkspaceViewForm: React.FC = ({ + handleFormSubmit, + handleClose, + status, + data, + preLoadedData, +}) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + reset, + watch, + setValue, + } = useForm({ + defaultValues, + }); + const filters = watch("query"); + + const { data: labelOptions } = useSWR( + workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null + ); + + const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + const memberOptions = workspaceMembers?.map((m) => m.member); + + const { projects: allProjects } = useProjects(); + const joinedProjects = allProjects?.filter((p) => p.is_member); + + const handleCreateUpdateView = async (formData: IWorkspaceView) => { + await handleFormSubmit(formData); + + reset({ + ...defaultValues, + }); + }; + + const clearAllFilters = () => { + setValue("query", { + assignees: null, + created_by: null, + subscriber: null, + labels: null, + priority: null, + state_group: null, + start_date: null, + target_date: null, + project: null, + }); + }; + + useEffect(() => { + reset({ + ...defaultValues, + ...preLoadedData, + ...data, + }); + }, [data, preLoadedData, reset]); + + useEffect(() => { + if (status && data) { + setValue("query", data.query_data); + } + }, [data, status, setValue]); + + return ( +
+
+

+ {status ? "Update" : "Create"} View +

+
+
+ +
+
+