From a048e513b7910c3d3db3cc53ae9bc29fdf38e174 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 28 Sep 2023 20:34:57 +0530 Subject: [PATCH 01/38] chore: workspace view display filters and properties , code refactor (#2295) * chore: spreadsheet view context * chore: spreadsheet context provider * chore: spreadsheet view context * chore: display filters and properties added in workspace view and code refactor * fix: build error fix * chore: set sub-issue display option to false for global views --------- Co-authored-by: gurusainath --- .../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 - .../core/views/spreadsheet-view/index.ts | 1 - .../issue-column/issue-column.tsx | 6 +- .../spreadsheet-view/spreadsheet-columns.tsx | 274 ------------- .../spreadsheet-view/spreadsheet-view.tsx | 354 +++++++++++++++-- .../state-column/state-column.tsx | 2 +- .../my-issues/my-issues-select-filters.tsx | 125 +----- .../workspace-views/workpace-view-issues.tsx | 221 +++++++++++ .../workspace-views/workspace-all-issue.tsx | 234 +++++++++++ .../workspace-assigned-issue.tsx | 155 ++++++++ .../workspace-created-issues.tsx | 147 +++++++ .../workspace-issue-view-option.tsx | 49 ++- .../workspace-subscribed-issue.tsx | 148 +++++++ .../profile/profile-issues-view-options.tsx | 81 ++-- web/components/ui/dropdowns/custom-menu.tsx | 8 +- web/components/ui/dropdowns/types.d.ts | 1 + web/components/views/delete-view-modal.tsx | 81 ++-- web/components/views/form.tsx | 33 +- web/components/views/modal.tsx | 153 +++----- 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 | 13 +- web/contexts/profile-issues-context.tsx | 1 + web/contexts/workspace-view-context.tsx | 235 +++++++++++ web/hooks/my-issues/use-my-issues-filter.tsx | 2 +- web/hooks/use-workspace-view.tsx | 11 + web/hooks/use-worskpace-issue-filter.tsx | 113 ------ .../project-authorization-wrapper.tsx | 5 +- .../workspace-authorization-wrapper.tsx | 81 ++-- .../projects/[projectId]/views/index.tsx | 2 - .../workspace-views/[workspaceViewId].tsx | 338 ++-------------- .../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/services/workspace.service.ts | 18 +- web/types/view-props.d.ts | 62 ++- web/types/views.d.ts | 12 +- web/types/workspace-views.d.ts | 22 ++ 48 files changed, 3119 insertions(+), 2369 deletions(-) create mode 100644 web/components/core/filters/workspace-filters-list.tsx delete mode 100644 web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx create mode 100644 web/components/issues/workspace-views/workpace-view-issues.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 create mode 100644 web/contexts/workspace-view-context.tsx create mode 100644 web/hooks/use-workspace-view.tsx delete mode 100644 web/hooks/use-worskpace-issue-filter.tsx create mode 100644 web/types/workspace-views.d.ts diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index 8e17cbafd..f183de9c6 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 50493c64f..697844a15 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 62bcd5e58..3c123c2ac 100644 --- a/web/components/core/views/issues-view.tsx +++ b/web/components/core/views/issues-view.tsx @@ -483,7 +483,6 @@ export const IssuesView: React.FC = ({ setCreateViewModal(null)} - viewType="project" preLoadedData={createViewModal} user={user} /> diff --git a/web/components/core/views/spreadsheet-view/index.ts b/web/components/core/views/spreadsheet-view/index.ts index e72819dad..9bf8ed1b0 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/issue-column/issue-column.tsx b/web/components/core/views/spreadsheet-view/issue-column/issue-column.tsx index bb36c0e60..1afcda5e1 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-columns.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx deleted file mode 100644 index f52f1ab38..000000000 --- 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 b17ce8d2d..088a4837d 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,13 +19,20 @@ 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"; +import { useWorkspaceView } from "hooks/use-workspace-view"; // types -import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types"; -import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; +import { + ICurrentUserResponse, + IIssue, + ISubIssueResponse, + TIssueOrderByOptions, + UserAuth, +} from "types"; import { CYCLE_DETAILS, CYCLE_ISSUES_WITH_PARAMS, @@ -39,7 +46,7 @@ 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"; type Props = { spreadsheetIssues: IIssue[]; @@ -70,6 +77,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 +88,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 +129,9 @@ export const SpreadsheetView: React.FC = ({ router.pathname.includes(path.path) ); - const { params: workspaceViewParams } = useWorkspaceIssuesFilters( - workspaceSlug?.toString(), - workspaceViewId?.toString() - ); + const { params: workspaceViewParams, handleFilters } = useWorkspaceView(); - const { params } = useSpreadsheetIssuesView(); + const { params, displayFilters, setDisplayFilters } = useSpreadsheetIssuesView(); const partialUpdateIssue = useCallback( (formData: Partial, issue: IIssue) => { @@ -199,8 +214,8 @@ export const SpreadsheetView: React.FC = ({ moduleId, viewId, workspaceViewId, - currentWorkspaceIssuePath, workspaceViewParams, + currentWorkspaceIssuePath, params, user, ] @@ -208,10 +223,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) + handleFilters("display_filters", { 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 +451,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 ( <> = ({ workspaceSlug={workspaceSlug?.toString() ?? ""} readOnly={disableUserActions} /> -
+
-
+
{spreadsheetIssues ? ( <>
-
+
- + ID @@ -270,15 +516,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/core/views/spreadsheet-view/state-column/state-column.tsx b/web/components/core/views/spreadsheet-view/state-column/state-column.tsx index a31345f3d..04c833b1d 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 8085b5e78..ce8e03797 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 new file mode 100644 index 000000000..708e0eb9f --- /dev/null +++ b/web/components/issues/workspace-views/workpace-view-issues.tsx @@ -0,0 +1,221 @@ +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-view"; +import useWorkspaceMembers from "hooks/use-workspace-members"; +// 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"; +// 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(); + const { workspaceSlug, workspaceViewId } = router.query; + + const { memberRole } = useProjectMyMembership(); + const { user } = useUser(); + const { isGuest, isViewer } = useWorkspaceMembers( + workspaceSlug?.toString(), + Boolean(workspaceSlug) + ); + const { filters, viewIssues, mutateViewIssues, handleFilters } = useWorkspaceView(); + + 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 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 isNotAllowed = isGuest || isViewer; + 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)} /> + {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) 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-all-issue.tsx b/web/components/issues/workspace-views/workspace-all-issue.tsx new file mode 100644 index 000000000..724b9d309 --- /dev/null +++ b/web/components/issues/workspace-views/workspace-all-issue.tsx @@ -0,0 +1,234 @@ +import { useCallback, useState } from "react"; +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// hook +import useUser from "hooks/use-user"; +import useWorkspaceMembers from "hooks/use-workspace-members"; +import useProjects from "hooks/use-projects"; +import { useWorkspaceView } from "hooks/use-workspace-view"; +// context +import { useProjectMyMembership } from "contexts/project-member.context"; +// services +import workspaceService from "services/workspace.service"; +import projectIssuesServices from "services/issues.service"; +// components +import { SpreadsheetView, WorkspaceFiltersList } 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"; +// ui +import { PrimaryButton } from "components/ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +// fetch-keys +import { WORKSPACE_LABELS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; +// constants +import { STATE_GROUP } from "constants/project"; +// types +import { IIssue, IWorkspaceIssueFilterOptions } from "types"; + +export const WorkspaceAllIssue = () => { + 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 000000000..ccee9ab84 --- /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 000000000..bcc83c38b --- /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 4e98cce92..b635ff870 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 @@ -33,12 +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, handleFilters } = useWorkspaceView(); const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues"); @@ -58,12 +52,12 @@ export const WorkspaceIssuesViewOptions: React.FC = () => { - ); - })} + return ( + + ); + })}
diff --git a/web/components/ui/dropdowns/custom-menu.tsx b/web/components/ui/dropdowns/custom-menu.tsx index c451d4432..41450b2b3 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 aace1858a..b368a7ed8 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/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index 0d49c62cc..61c627430 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 29d16ca9c..0c57a9542 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 03f4f0b60..c1ff54231 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,11 +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 { VIEWS_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; - viewType: "project" | "workspace"; handleClose: () => void; data?: IView | null; preLoadedData?: Partial | null; @@ -29,7 +27,6 @@ type Props = { export const CreateUpdateViewModal: React.FC = ({ isOpen, - viewType, handleClose, data, preLoadedData, @@ -49,48 +46,25 @@ export const CreateUpdateViewModal: React.FC = ({ ...payload, query_data: payload.query, }; + await viewsService + .createView(workspaceSlug as string, projectId as string, payload, user) + .then(() => { + mutate(VIEWS_LIST(projectId as string)); + handleClose(); - if (viewType === "project") { - await viewsService - .createView(workspaceSlug as string, projectId as string, payload, user) - .then(() => { - mutate(VIEWS_LIST(projectId as string)); - handleClose(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "View created successfully.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be created. Please try again.", - }); + 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(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "View created successfully.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be created. Please try again.", - }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be created. Please try again.", }); - } + }); }; const updateView = async (payload: IView) => { @@ -98,79 +72,41 @@ export const CreateUpdateViewModal: React.FC = ({ ...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 }; + await viewsService + .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) + .then((res) => { + mutate( + VIEWS_LIST(projectId as string), + (prevData) => + prevData?.map((p) => { + if (p.id === res.id) return { ...p, ...payloadData }; - return p; - }), - false - ); - onClose(); + return p; + }), + false + ); + onClose(); - setToastAlert({ - type: "success", - title: "Success!", - message: "View updated successfully.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "View could not be updated. Please try again.", - }); + setToastAlert({ + type: "success", + title: "Success!", + message: "View updated successfully.", }); - } 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.", - }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "View could not be updated. Please try again.", }); - } + }); }; const handleFormSubmit = async (formData: IView) => { - if (viewType === "project") { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId) return; - if (!data) await createView(formData); - else await updateView(formData); - } else { - if (!workspaceSlug) return; - - if (!data) await createView(formData); - else await updateView(formData); - } + if (!data) await createView(formData); + else await updateView(formData); }; return ( @@ -205,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 7b1324f5f..c3aadc33d 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 d27eb3cf1..a2e0d213a 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 000000000..6030f630f --- /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 000000000..b16d61399 --- /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 +

+
+
+ +
+
+