From 3d7fe40035a57459dec2c373018f285843730226 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:51:26 +0530 Subject: [PATCH] feat: my issues view layouts and filters, refactor: issue views (#1681) * refactor: issue views and my issues * chore: update view dropdown options * refactor: render emoji function * refactor: api calss * fix: build errors * fix: fetch states only when dropdown is opened * chore: my issues dnd * fix: build errors * refactor: folder structure --- .../analytics/custom-analytics/sidebar.tsx | 14 +- .../components/core/filters/filters-list.tsx | 73 ++--- .../core/filters/issues-view-filter.tsx | 11 +- apps/app/components/core/index.ts | 11 +- .../components/core/list-view/all-lists.tsx | 72 ----- apps/app/components/core/views/all-views.tsx | 203 ++++++++++++ .../{ => views}/board-view/all-boards.tsx | 65 ++-- .../{ => views}/board-view/board-header.tsx | 63 ++-- .../core/{ => views}/board-view/index.ts | 0 .../{ => views}/board-view/single-board.tsx | 71 ++--- .../{ => views}/board-view/single-issue.tsx | 35 +-- .../calendar-view/calendar-header.tsx | 0 .../{ => views}/calendar-view/calendar.tsx | 16 +- .../core/{ => views}/calendar-view/index.ts | 0 .../{ => views}/calendar-view/single-date.tsx | 10 +- .../calendar-view/single-issue.tsx | 0 .../{ => views}/gantt-chart-view/index.tsx | 0 apps/app/components/core/views/index.ts | 7 + .../core/{ => views}/issues-view.tsx | 250 +++++---------- .../core/views/list-view/all-lists.tsx | 62 ++++ .../core/{ => views}/list-view/index.ts | 0 .../{ => views}/list-view/single-issue.tsx | 35 ++- .../{ => views}/list-view/single-list.tsx | 82 +++-- .../{ => views}/spreadsheet-view/index.ts | 0 .../spreadsheet-view/single-issue.tsx | 6 +- .../spreadsheet-view/spreadsheet-columns.tsx | 0 .../spreadsheet-view/spreadsheet-issues.tsx | 25 +- .../spreadsheet-view/spreadsheet-view.tsx | 23 +- .../app/components/icons/state-group-icon.tsx | 42 ++- .../app/components/inbox/filters-dropdown.tsx | 50 ++- .../components/issues/delete-issue-modal.tsx | 6 +- apps/app/components/issues/index.ts | 2 +- .../components/issues/my-issues-list-item.tsx | 218 ------------- apps/app/components/issues/my-issues/index.ts | 3 + .../my-issues/my-issues-select-filters.tsx | 168 ++++++++++ .../my-issues/my-issues-view-options.tsx | 290 ++++++++++++++++++ .../issues/my-issues/my-issues-view.tsx | 288 +++++++++++++++++ .../components/issues/view-select/state.tsx | 11 +- .../project/create-project-modal.tsx | 15 +- .../project/single-project-card.tsx | 7 +- .../project/single-sidebar-project.tsx | 7 +- .../ui/dropdowns/custom-search-select.tsx | 213 ++++++------- apps/app/components/ui/dropdowns/types.d.ts | 1 + .../components/ui/multi-level-dropdown.tsx | 79 +++-- apps/app/components/views/form.tsx | 94 +++++- apps/app/components/views/select-filters.tsx | 175 +++++------ apps/app/constants/fetch-keys.ts | 33 +- apps/app/constants/issue.ts | 4 +- apps/app/contexts/issue-view.context.tsx | 2 - apps/app/helpers/emoji.helper.ts | 25 -- apps/app/helpers/emoji.helper.tsx | 38 +++ .../hooks/my-issues/use-my-issues-filter.tsx | 201 ++++++++++++ apps/app/hooks/my-issues/use-my-issues.tsx | 61 ++++ apps/app/hooks/use-calendar-issues-view.tsx | 6 - apps/app/hooks/use-issues-view.tsx | 6 - apps/app/hooks/use-issues.tsx | 22 -- apps/app/hooks/use-my-issues-filter.tsx | 104 ------- apps/app/hooks/use-project-members.tsx | 9 +- .../app/hooks/use-spreadsheet-issues-view.tsx | 6 - apps/app/hooks/use-workspace-members.tsx | 8 +- .../pages/[workspaceSlug]/me/my-issues.tsx | 220 ++++--------- .../projects/[projectId]/cycles/[cycleId].tsx | 13 +- .../[projectId]/modules/[moduleId].tsx | 2 +- .../projects/[projectId]/settings/index.tsx | 17 +- apps/app/services/issues.service.ts | 8 + apps/app/services/user.service.ts | 15 +- apps/app/services/workspace.service.ts | 6 +- apps/app/types/issues.d.ts | 22 +- apps/app/types/views.d.ts | 23 +- apps/app/types/workspace.d.ts | 25 +- 70 files changed, 2249 insertions(+), 1430 deletions(-) delete mode 100644 apps/app/components/core/list-view/all-lists.tsx create mode 100644 apps/app/components/core/views/all-views.tsx rename apps/app/components/core/{ => views}/board-view/all-boards.tsx (66%) rename apps/app/components/core/{ => views}/board-view/board-header.tsx (69%) rename apps/app/components/core/{ => views}/board-view/index.ts (100%) rename apps/app/components/core/{ => views}/board-view/single-board.tsx (78%) rename apps/app/components/core/{ => views}/board-view/single-issue.tsx (95%) rename apps/app/components/core/{ => views}/calendar-view/calendar-header.tsx (100%) rename apps/app/components/core/{ => views}/calendar-view/calendar.tsx (95%) rename apps/app/components/core/{ => views}/calendar-view/index.ts (100%) rename apps/app/components/core/{ => views}/calendar-view/single-date.tsx (92%) rename apps/app/components/core/{ => views}/calendar-view/single-issue.tsx (100%) rename apps/app/components/core/{ => views}/gantt-chart-view/index.tsx (100%) create mode 100644 apps/app/components/core/views/index.ts rename apps/app/components/core/{ => views}/issues-view.tsx (67%) create mode 100644 apps/app/components/core/views/list-view/all-lists.tsx rename apps/app/components/core/{ => views}/list-view/index.ts (100%) rename apps/app/components/core/{ => views}/list-view/single-issue.tsx (93%) rename apps/app/components/core/{ => views}/list-view/single-list.tsx (77%) rename apps/app/components/core/{ => views}/spreadsheet-view/index.ts (100%) rename apps/app/components/core/{ => views}/spreadsheet-view/single-issue.tsx (99%) rename apps/app/components/core/{ => views}/spreadsheet-view/spreadsheet-columns.tsx (100%) rename apps/app/components/core/{ => views}/spreadsheet-view/spreadsheet-issues.tsx (80%) rename apps/app/components/core/{ => views}/spreadsheet-view/spreadsheet-view.tsx (91%) delete mode 100644 apps/app/components/issues/my-issues-list-item.tsx create mode 100644 apps/app/components/issues/my-issues/index.ts create mode 100644 apps/app/components/issues/my-issues/my-issues-select-filters.tsx create mode 100644 apps/app/components/issues/my-issues/my-issues-view-options.tsx create mode 100644 apps/app/components/issues/my-issues/my-issues-view.tsx delete mode 100644 apps/app/helpers/emoji.helper.ts create mode 100644 apps/app/helpers/emoji.helper.tsx create mode 100644 apps/app/hooks/my-issues/use-my-issues-filter.tsx create mode 100644 apps/app/hooks/my-issues/use-my-issues.tsx delete mode 100644 apps/app/hooks/use-issues.tsx delete mode 100644 apps/app/hooks/use-my-issues-filter.tsx diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/apps/app/components/analytics/custom-analytics/sidebar.tsx index 6d413a9c9..d1a29da41 100644 --- a/apps/app/components/analytics/custom-analytics/sidebar.tsx +++ b/apps/app/components/analytics/custom-analytics/sidebar.tsx @@ -227,12 +227,7 @@ export const AnalyticsSidebar: React.FC = ({ ) : project.icon_prop ? (
- - {project.icon_prop.name} - + {renderEmoji(project.icon_prop)}
) : ( @@ -342,12 +337,7 @@ export const AnalyticsSidebar: React.FC = ({ ) : projectDetails?.icon_prop ? (
- - {projectDetails.icon_prop.name} - + {renderEmoji(projectDetails.icon_prop)}
) : ( diff --git a/apps/app/components/core/filters/filters-list.tsx b/apps/app/components/core/filters/filters-list.tsx index b7928de68..cf9c9dc4b 100644 --- a/apps/app/components/core/filters/filters-list.tsx +++ b/apps/app/components/core/filters/filters-list.tsx @@ -1,6 +1,6 @@ import React from "react"; + import { useRouter } from "next/router"; -import useSWR from "swr"; // icons import { XMarkIcon } from "@heroicons/react/24/outline"; @@ -8,42 +8,31 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons"; // ui import { Avatar } from "components/ui"; // helpers -import { getStatesList } from "helpers/state.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; -// services -import issuesService from "services/issues.service"; -import projectService from "services/project.service"; -import stateService from "services/state.service"; -// types -import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys"; -import { IIssueFilterOptions } from "types"; +// helpers import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; +// types +import { IIssueFilterOptions, IIssueLabels, IState, IUserLite } from "types"; -export const FilterList: React.FC = ({ filters, setFilters }) => { +type Props = { + filters: any; + setFilters: any; + clearAllFilters: (...args: any) => void; + labels: IIssueLabels[] | undefined; + members: IUserLite[] | undefined; + states: IState[] | undefined; +}; + +export const FiltersList: React.FC = ({ + filters, + setFilters, + clearAllFilters, + labels, + members, + states, +}) => { const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query; - - const { data: members } = useSWR( - projectId ? PROJECT_MEMBERS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) - : null - ); - - const { data: issueLabels } = useSWR( - projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, - workspaceSlug && projectId - ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId.toString()) - : null - ); - - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const states = getStatesList(stateGroups ?? {}); + const { viewId } = router.query; if (!filters) return <>; @@ -166,7 +155,7 @@ export const FilterList: React.FC = ({ filters, setFilters }) => { ) : key === "assignees" ? (
{filters.assignees?.map((memberId: string) => { - const member = members?.find((m) => m.member.id === memberId)?.member; + const member = members?.find((m) => m.id === memberId); return (
= ({ filters, setFilters }) => { ) : key === "created_by" ? (
{filters.created_by?.map((memberId: string) => { - const member = members?.find((m) => m.member.id === memberId)?.member; + const member = members?.find((m) => m.id === memberId); return (
= ({ filters, setFilters }) => { ) : key === "labels" ? (
{filters.labels?.map((labelId: string) => { - const label = issueLabels?.find((l) => l.id === labelId); + const label = labels?.find((l) => l.id === labelId); if (!label) return null; const color = label.color !== "" ? label.color : "#0f172a"; @@ -370,17 +359,7 @@ export const FilterList: React.FC = ({ filters, setFilters }) => { {Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index b91944abf..543feaed3 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -1,12 +1,7 @@ -export * from "./board-view"; -export * from "./calendar-view"; export * from "./filters"; -export * from "./gantt-chart-view"; -export * from "./list-view"; export * from "./modals"; -export * from "./spreadsheet-view"; -export * from "./theme"; export * from "./sidebar"; -export * from "./issues-view"; -export * from "./image-picker-popover"; +export * from "./theme"; +export * from "./views"; export * from "./feeds"; +export * from "./image-picker-popover"; diff --git a/apps/app/components/core/list-view/all-lists.tsx b/apps/app/components/core/list-view/all-lists.tsx deleted file mode 100644 index fcedf169a..000000000 --- a/apps/app/components/core/list-view/all-lists.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// hooks -import useIssuesView from "hooks/use-issues-view"; -// components -import { SingleList } from "components/core/list-view/single-list"; -// types -import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; - -// types -type Props = { - type: "issue" | "cycle" | "module"; - states: IState[] | undefined; - addIssueToState: (groupTitle: string) => void; - makeIssueCopy: (issue: IIssue) => void; - handleEditIssue: (issue: IIssue) => void; - handleDeleteIssue: (issue: IIssue) => void; - openIssuesListModal?: (() => void) | null; - removeIssue: ((bridgeId: string, issueId: string) => void) | null; - isCompleted?: boolean; - user: ICurrentUserResponse | undefined; - userAuth: UserAuth; -}; - -export const AllLists: React.FC = ({ - type, - states, - addIssueToState, - makeIssueCopy, - openIssuesListModal, - handleEditIssue, - handleDeleteIssue, - removeIssue, - isCompleted = false, - user, - userAuth, -}) => { - const { groupedByIssues, groupByProperty: selectedGroup, showEmptyGroups } = useIssuesView(); - - return ( - <> - {groupedByIssues && ( -
- {Object.keys(groupedByIssues).map((singleGroup) => { - const currentState = - selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; - - if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null; - - return ( - addIssueToState(singleGroup)} - makeIssueCopy={makeIssueCopy} - handleEditIssue={handleEditIssue} - handleDeleteIssue={handleDeleteIssue} - openIssuesListModal={type !== "issue" ? openIssuesListModal : null} - removeIssue={removeIssue} - isCompleted={isCompleted} - user={user} - userAuth={userAuth} - /> - ); - })} -
- )} - - ); -}; diff --git a/apps/app/components/core/views/all-views.tsx b/apps/app/components/core/views/all-views.tsx new file mode 100644 index 000000000..b37cb433b --- /dev/null +++ b/apps/app/components/core/views/all-views.tsx @@ -0,0 +1,203 @@ +import React, { useCallback } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// react-beautiful-dnd +import { DragDropContext, DropResult } from "react-beautiful-dnd"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +// services +import stateService from "services/state.service"; +// hooks +import useUser from "hooks/use-user"; +import { useProjectMyMembership } from "contexts/project-member.context"; +// components +import { + AllLists, + AllBoards, + CalendarView, + SpreadsheetView, + GanttChartView, +} from "components/core"; +// ui +import { EmptyState, SecondaryButton, Spinner } from "components/ui"; +// icons +import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; +// images +import emptyIssue from "public/empty-state/issue.svg"; +import emptyIssueArchive from "public/empty-state/issue-archive.svg"; +// helpers +import { getStatesList } from "helpers/state.helper"; +// types +import { IIssue, IIssueViewProps } from "types"; +// fetch-keys +import { STATES_LIST } from "constants/fetch-keys"; + +type Props = { + addIssueToDate: (date: string) => void; + addIssueToGroup: (groupTitle: string) => void; + disableUserActions: boolean; + dragDisabled?: boolean; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; + handleOnDragEnd: (result: DropResult) => Promise; + openIssuesListModal: (() => void) | null; + removeIssue: ((bridgeId: string, issueId: string) => void) | null; + trashBox: boolean; + setTrashBox: React.Dispatch>; + viewProps: IIssueViewProps; +}; + +export const AllViews: React.FC = ({ + addIssueToDate, + addIssueToGroup, + disableUserActions, + dragDisabled = false, + handleIssueAction, + handleOnDragEnd, + openIssuesListModal, + removeIssue, + trashBox, + setTrashBox, + viewProps, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + + const { user } = useUser(); + const { memberRole } = useProjectMyMembership(); + + const { groupedIssues, isEmpty, issueView } = viewProps; + + const { data: stateGroups } = useSWR( + workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, + workspaceSlug + ? () => stateService.getStates(workspaceSlug as string, projectId as string) + : null + ); + const states = getStatesList(stateGroups ?? {}); + + const handleTrashBox = useCallback( + (isDragging: boolean) => { + if (isDragging && !trashBox) setTrashBox(true); + }, + [trashBox, setTrashBox] + ); + + return ( + + + {(provided, snapshot) => ( +
+ + Drop here to delete the issue. +
+ )} +
+ {groupedIssues ? ( + !isEmpty || issueView === "kanban" || issueView === "calendar" ? ( + <> + {issueView === "list" ? ( + + ) : issueView === "kanban" ? ( + + ) : issueView === "calendar" ? ( + + ) : issueView === "spreadsheet" ? ( + + ) : ( + issueView === "gantt_chart" && + )} + + ) : router.pathname.includes("archived-issues") ? ( + { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`); + }} + /> + ) : ( + } + secondaryButton={ + cycleId || moduleId ? ( + {})} + > + + Add an existing issue + + ) : null + } + onClick={() => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }} + /> + ) + ) : ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/views/board-view/all-boards.tsx similarity index 66% rename from apps/app/components/core/board-view/all-boards.tsx rename to apps/app/components/core/views/board-view/all-boards.tsx index 003083aa9..ee0fc668b 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/views/board-view/all-boards.tsx @@ -1,75 +1,66 @@ -// hooks -import useProjectIssuesView from "hooks/use-issues-view"; // components -import { SingleBoard } from "components/core/board-view/single-board"; +import { SingleBoard } from "components/core/views/board-view/single-board"; // icons import { getStateGroupIcon } from "components/icons"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types"; type Props = { - type: "issue" | "cycle" | "module"; - states: IState[] | undefined; - addIssueToState: (groupTitle: string) => void; - makeIssueCopy: (issue: IIssue) => void; - handleEditIssue: (issue: IIssue) => void; - openIssuesListModal?: (() => void) | null; - handleDeleteIssue: (issue: IIssue) => void; + addIssueToGroup: (groupTitle: string) => void; + disableUserActions: boolean; + dragDisabled: boolean; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleTrashBox: (isDragging: boolean) => void; + openIssuesListModal?: (() => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null; - isCompleted?: boolean; + states: IState[] | undefined; user: ICurrentUserResponse | undefined; userAuth: UserAuth; + viewProps: IIssueViewProps; }; export const AllBoards: React.FC = ({ - type, - states, - addIssueToState, - makeIssueCopy, - handleEditIssue, - openIssuesListModal, - handleDeleteIssue, + addIssueToGroup, + disableUserActions, + dragDisabled, + handleIssueAction, handleTrashBox, + openIssuesListModal, removeIssue, - isCompleted = false, + states, user, userAuth, + viewProps, }) => { - const { - groupedByIssues, - groupByProperty: selectedGroup, - showEmptyGroups, - } = useProjectIssuesView(); + const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps; return ( <> - {groupedByIssues ? ( + {groupedIssues ? (
- {Object.keys(groupedByIssues).map((singleGroup, index) => { + {Object.keys(groupedIssues).map((singleGroup, index) => { const currentState = selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; - if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null; + if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null; return ( addIssueToGroup(singleGroup)} currentState={currentState} + disableUserActions={disableUserActions} + dragDisabled={dragDisabled} groupTitle={singleGroup} - handleEditIssue={handleEditIssue} - makeIssueCopy={makeIssueCopy} - addIssueToState={() => addIssueToState(singleGroup)} - handleDeleteIssue={handleDeleteIssue} - openIssuesListModal={openIssuesListModal ?? null} + handleIssueAction={handleIssueAction} handleTrashBox={handleTrashBox} + openIssuesListModal={openIssuesListModal ?? null} removeIssue={removeIssue} - isCompleted={isCompleted} user={user} userAuth={userAuth} + viewProps={viewProps} /> ); })} @@ -77,11 +68,11 @@ export const AllBoards: React.FC = ({

Hidden groups

- {Object.keys(groupedByIssues).map((singleGroup, index) => { + {Object.keys(groupedIssues).map((singleGroup, index) => { const currentState = selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; - if (groupedByIssues[singleGroup].length === 0) + if (groupedIssues[singleGroup].length === 0) return (
void; + addIssueToGroup: () => void; isCollapsed: boolean; setIsCollapsed: React.Dispatch>; - isCompleted?: boolean; + disableUserActions: boolean; + viewProps: IIssueViewProps; }; export const BoardHeader: React.FC = ({ currentState, groupTitle, - addIssueToState, + addIssueToGroup, isCollapsed, setIsCollapsed, - isCompleted = false, + disableUserActions, + viewProps, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView(); + const { groupedIssues, groupByProperty: selectedGroup } = viewProps; - const { data: issueLabels } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, - workspaceSlug && projectId - ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) + const { data: issueLabels } = useSWR( + workspaceSlug && projectId && selectedGroup === "labels" + ? PROJECT_ISSUE_LABELS(projectId.toString()) + : null, + workspaceSlug && projectId && selectedGroup === "labels" + ? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString()) : null ); const { data: members } = useSWR( - workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + workspaceSlug && projectId && selectedGroup === "created_by" + ? PROJECT_MEMBERS(projectId.toString()) + : null, + workspaceSlug && projectId && selectedGroup === "created_by" + ? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString()) : null ); + const { projects } = useProjects(); + const getGroupTitle = () => { let title = addSpaceIfCamelCase(groupTitle); @@ -67,6 +76,9 @@ export const BoardHeader: React.FC = ({ case "labels": title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None"; break; + case "project": + title = projects?.find((p) => p.id === groupTitle)?.name ?? "None"; + break; case "created_by": const member = members?.find((member) => member.member.id === groupTitle)?.member; title = @@ -87,9 +99,22 @@ export const BoardHeader: React.FC = ({ icon = currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); break; + case "state_detail.group": + icon = getStateGroupIcon(groupTitle as any, "16", "16"); + break; case "priority": icon = getPriorityIcon(groupTitle, "text-lg"); break; + case "project": + const project = projects?.find((p) => p.id === groupTitle); + icon = + project && + (project.emoji !== null + ? renderEmoji(project.emoji) + : project.icon_prop !== null + ? renderEmoji(project.icon_prop) + : null); + break; case "labels": const labelColor = issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000"; @@ -116,7 +141,7 @@ export const BoardHeader: React.FC = ({ !isCollapsed ? "flex-col rounded-md bg-custom-background-90" : "" }`} > -
+
= ({

{getGroupTitle()} @@ -136,7 +161,7 @@ export const BoardHeader: React.FC = ({ isCollapsed ? "ml-0.5" : "" } min-w-[2.5rem] rounded-full bg-custom-background-80 py-1 text-center text-xs`} > - {groupedByIssues?.[groupTitle].length ?? 0} + {groupedIssues?.[groupTitle].length ?? 0}

@@ -155,11 +180,11 @@ export const BoardHeader: React.FC = ({ )} - {!isCompleted && selectedGroup !== "created_by" && ( + {!disableUserActions && selectedGroup !== "created_by" && ( diff --git a/apps/app/components/core/board-view/index.ts b/apps/app/components/core/views/board-view/index.ts similarity index 100% rename from apps/app/components/core/board-view/index.ts rename to apps/app/components/core/views/board-view/index.ts diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/views/board-view/single-board.tsx similarity index 78% rename from apps/app/components/core/board-view/single-board.tsx rename to apps/app/components/core/views/board-view/single-board.tsx index 5701f4c58..3380dd1ec 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/views/board-view/single-board.tsx @@ -5,9 +5,6 @@ import { useRouter } from "next/router"; // react-beautiful-dnd import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { Draggable } from "react-beautiful-dnd"; -// hooks -import useIssuesView from "hooks/use-issues-view"; -import useIssuesProperties from "hooks/use-issue-properties"; // components import { BoardHeader, SingleBoardIssue } from "components/core"; // ui @@ -17,64 +14,63 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types"; type Props = { - type?: "issue" | "cycle" | "module"; + addIssueToGroup: () => void; currentState?: IState | null; + disableUserActions: boolean; + dragDisabled: boolean; groupTitle: string; - handleEditIssue: (issue: IIssue) => void; - makeIssueCopy: (issue: IIssue) => void; - addIssueToState: () => void; - handleDeleteIssue: (issue: IIssue) => void; - openIssuesListModal?: (() => void) | null; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleTrashBox: (isDragging: boolean) => void; + openIssuesListModal?: (() => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null; - isCompleted?: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; + viewProps: IIssueViewProps; }; export const SingleBoard: React.FC = ({ - type, + addIssueToGroup, currentState, groupTitle, - handleEditIssue, - makeIssueCopy, - addIssueToState, - handleDeleteIssue, - openIssuesListModal, + disableUserActions, + dragDisabled, + handleIssueAction, handleTrashBox, + openIssuesListModal, removeIssue, - isCompleted = false, user, userAuth, + viewProps, }) => { // collapse/expand const [isCollapsed, setIsCollapsed] = useState(true); - const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView(); + const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps; const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { cycleId, moduleId } = router.query; - const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); + const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; // Check if it has at least 4 tickets since it is enough to accommodate the Calendar height - const issuesLength = groupedByIssues?.[groupTitle].length; + const issuesLength = groupedIssues?.[groupTitle].length; const hasMinimumNumberOfCards = issuesLength ? issuesLength >= 4 : false; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; return (
{isCollapsed && ( @@ -112,14 +108,12 @@ export const SingleBoard: React.FC = ({ hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : "" } `} > - {groupedByIssues?.[groupTitle].map((issue, index) => ( + {groupedIssues?.[groupTitle].map((issue, index) => ( {(provided, snapshot) => ( = ({ snapshot={snapshot} type={type} index={index} - selectedGroup={selectedGroup} issue={issue} groupTitle={groupTitle} - properties={properties} - editIssue={() => handleEditIssue(issue)} - makeIssueCopy={() => makeIssueCopy(issue)} - handleDeleteIssue={handleDeleteIssue} + editIssue={() => handleIssueAction(issue, "edit")} + makeIssueCopy={() => handleIssueAction(issue, "copy")} + handleDeleteIssue={() => handleIssueAction(issue, "delete")} handleTrashBox={handleTrashBox} removeIssue={() => { if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id, issue.id); }} - isCompleted={isCompleted} + disableUserActions={disableUserActions} user={user} userAuth={userAuth} + viewProps={viewProps} /> )} @@ -161,18 +154,18 @@ export const SingleBoard: React.FC = ({ ) : ( - !isCompleted && ( + !disableUserActions && ( Add Issue @@ -181,7 +174,7 @@ export const SingleBoard: React.FC = ({ position="left" noBorder > - + Create new {openIssuesListModal && ( diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/views/board-view/single-issue.tsx similarity index 95% rename from apps/app/components/core/board-view/single-issue.tsx rename to apps/app/components/core/views/board-view/single-issue.tsx index bab90c537..5deaabe06 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/views/board-view/single-issue.tsx @@ -15,7 +15,6 @@ import { // services import issuesService from "services/issues.service"; // hooks -import useIssuesView from "hooks/use-issues-view"; import useToast from "hooks/use-toast"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components @@ -45,14 +44,7 @@ import { LayerDiagonalIcon } from "components/icons"; import { handleIssuesMutation } from "constants/issue"; import { copyTextToClipboard } from "helpers/string.helper"; // types -import { - ICurrentUserResponse, - IIssue, - ISubIssueResponse, - Properties, - TIssueGroupByOptions, - UserAuth, -} from "types"; +import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -61,6 +53,7 @@ import { MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, SUB_ISSUES, + USER_ISSUES, VIEW_ISSUES, } from "constants/fetch-keys"; @@ -69,18 +62,17 @@ type Props = { provided: DraggableProvided; snapshot: DraggableStateSnapshot; issue: IIssue; - properties: Properties; groupTitle?: string; index: number; - selectedGroup: TIssueGroupByOptions; editIssue: () => void; makeIssueCopy: () => void; removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; handleTrashBox: (isDragging: boolean) => void; - isCompleted?: boolean; + disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; + viewProps: IIssueViewProps; }; export const SingleBoardIssue: React.FC = ({ @@ -88,18 +80,17 @@ export const SingleBoardIssue: React.FC = ({ provided, snapshot, issue, - properties, index, - selectedGroup, editIssue, makeIssueCopy, removeIssue, groupTitle, handleDeleteIssue, handleTrashBox, - isCompleted = false, + disableUserActions, user, userAuth, + viewProps, }) => { // context menu const [contextMenu, setContextMenu] = useState(false); @@ -108,7 +99,7 @@ export const SingleBoardIssue: React.FC = ({ const actionSectionRef = useRef(null); - const { orderBy, params } = useIssuesView(); + const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps; const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; @@ -117,7 +108,7 @@ export const SingleBoardIssue: React.FC = ({ const partialUpdateIssue = useCallback( (formData: Partial, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !issue) return; const fetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) @@ -125,7 +116,9 @@ export const SingleBoardIssue: React.FC = ({ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : viewId ? VIEW_ISSUES(viewId.toString(), params) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); + : router.pathname.includes("my-issues") + ? USER_ISSUES(workspaceSlug.toString(), params) + : PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params); if (issue.parent) { mutate( @@ -170,7 +163,7 @@ export const SingleBoardIssue: React.FC = ({ } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) + .patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user) .then(() => { mutate(fetchKey); @@ -180,7 +173,6 @@ export const SingleBoardIssue: React.FC = ({ }, [ workspaceSlug, - projectId, cycleId, moduleId, viewId, @@ -189,6 +181,7 @@ export const SingleBoardIssue: React.FC = ({ selectedGroup, orderBy, params, + router, user, ] ); @@ -228,7 +221,7 @@ export const SingleBoardIssue: React.FC = ({ useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; return ( <> diff --git a/apps/app/components/core/calendar-view/calendar-header.tsx b/apps/app/components/core/views/calendar-view/calendar-header.tsx similarity index 100% rename from apps/app/components/core/calendar-view/calendar-header.tsx rename to apps/app/components/core/views/calendar-view/calendar-header.tsx diff --git a/apps/app/components/core/calendar-view/calendar.tsx b/apps/app/components/core/views/calendar-view/calendar.tsx similarity index 95% rename from apps/app/components/core/calendar-view/calendar.tsx rename to apps/app/components/core/views/calendar-view/calendar.tsx index 29d6ae446..5a8a07260 100644 --- a/apps/app/components/core/calendar-view/calendar.tsx +++ b/apps/app/components/core/views/calendar-view/calendar.tsx @@ -34,19 +34,17 @@ import { } from "constants/fetch-keys"; type Props = { - handleEditIssue: (issue: IIssue) => void; - handleDeleteIssue: (issue: IIssue) => void; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; addIssueToDate: (date: string) => void; - isCompleted: boolean; + disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; export const CalendarView: React.FC = ({ - handleEditIssue, - handleDeleteIssue, + handleIssueAction, addIssueToDate, - isCompleted = false, + disableUserActions, user, userAuth, }) => { @@ -167,7 +165,7 @@ export const CalendarView: React.FC = ({ ); }, [currentDate]); - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; return calendarIssues ? (
@@ -220,10 +218,10 @@ export const CalendarView: React.FC = ({ > {currentViewDaysData.map((date, index) => ( void; - handleDeleteIssue: (issue: IIssue) => void; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; index: number; date: { date: string; @@ -28,8 +27,7 @@ type Props = { }; export const SingleCalendarDate: React.FC = ({ - handleEditIssue, - handleDeleteIssue, + handleIssueAction, date, index, addIssueToDate, @@ -72,8 +70,8 @@ export const SingleCalendarDate: React.FC = ({ provided={provided} snapshot={snapshot} issue={issue} - handleEditIssue={handleEditIssue} - handleDeleteIssue={handleDeleteIssue} + handleEditIssue={() => handleIssueAction(issue, "edit")} + handleDeleteIssue={() => handleIssueAction(issue, "delete")} user={user} isNotAllowed={isNotAllowed} /> diff --git a/apps/app/components/core/calendar-view/single-issue.tsx b/apps/app/components/core/views/calendar-view/single-issue.tsx similarity index 100% rename from apps/app/components/core/calendar-view/single-issue.tsx rename to apps/app/components/core/views/calendar-view/single-issue.tsx diff --git a/apps/app/components/core/gantt-chart-view/index.tsx b/apps/app/components/core/views/gantt-chart-view/index.tsx similarity index 100% rename from apps/app/components/core/gantt-chart-view/index.tsx rename to apps/app/components/core/views/gantt-chart-view/index.tsx diff --git a/apps/app/components/core/views/index.ts b/apps/app/components/core/views/index.ts new file mode 100644 index 000000000..8b2dc87cb --- /dev/null +++ b/apps/app/components/core/views/index.ts @@ -0,0 +1,7 @@ +export * from "./board-view"; +export * from "./calendar-view"; +export * from "./gantt-chart-view"; +export * from "./list-view"; +export * from "./spreadsheet-view"; +export * from "./all-views"; +export * from "./issues-view"; diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/views/issues-view.tsx similarity index 67% rename from apps/app/components/core/issues-view.tsx rename to apps/app/components/core/views/issues-view.tsx index 826ff516d..a84aab1d2 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/views/issues-view.tsx @@ -5,38 +5,26 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; // react-beautiful-dnd -import { DragDropContext, DropResult } from "react-beautiful-dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { DropResult } from "react-beautiful-dnd"; // services import issuesService from "services/issues.service"; import stateService from "services/state.service"; import modulesService from "services/modules.service"; import trackEventServices from "services/track-event.service"; -// contexts -import { useProjectMyMembership } from "contexts/project-member.context"; // hooks import useToast from "hooks/use-toast"; import useIssuesView from "hooks/use-issues-view"; import useUserAuth from "hooks/use-user-auth"; +import useIssuesProperties from "hooks/use-issue-properties"; +import useProjectMembers from "hooks/use-project-members"; // components -import { - AllLists, - AllBoards, - FilterList, - CalendarView, - GanttChartView, - SpreadsheetView, -} from "components/core"; +import { FiltersList, AllViews } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateViewModal } from "components/views"; -import { TransferIssues, TransferIssuesModal } from "components/cycles"; // ui -import { EmptyState, PrimaryButton, Spinner, SecondaryButton } from "components/ui"; +import { PrimaryButton } from "components/ui"; // icons -import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; -// images -import emptyIssue from "public/empty-state/issue.svg"; -import emptyIssueArchive from "public/empty-state/issue-archive.svg"; +import { PlusIcon } from "@heroicons/react/24/outline"; // helpers import { getStatesList } from "helpers/state.helper"; import { orderArrayBy } from "helpers/array.helper"; @@ -49,19 +37,18 @@ import { MODULE_DETAILS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, + PROJECT_ISSUE_LABELS, STATES_LIST, } from "constants/fetch-keys"; type Props = { - type?: "issue" | "cycle" | "module"; openIssuesListModal?: () => void; - isCompleted?: boolean; + disableUserActions?: boolean; }; export const IssuesView: React.FC = ({ - type = "issue", openIssuesListModal, - isCompleted = false, + disableUserActions = false, }) => { // create issue modal const [createIssueModal, setCreateIssueModal] = useState(false); @@ -83,14 +70,9 @@ export const IssuesView: React.FC = ({ // trash box const [trashBox, setTrashBox] = useState(false); - // transfer issue - const [transferIssuesModal, setTransferIssuesModal] = useState(false); - const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; - const { memberRole } = useProjectMyMembership(); - const { user } = useUserAuth(); const { setToastAlert } = useToast(); @@ -104,7 +86,9 @@ export const IssuesView: React.FC = ({ isEmpty, setFilters, params, + showEmptyGroups, } = useIssuesView(); + const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, @@ -114,6 +98,15 @@ export const IssuesView: React.FC = ({ ); const states = getStatesList(stateGroups ?? {}); + const { data: labels } = useSWR( + workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, + workspaceSlug && projectId + ? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString()) + : null + ); + + const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString()); + const handleDeleteIssue = useCallback( (issue: IIssue) => { setDeleteIssueModal(true); @@ -123,7 +116,7 @@ export const IssuesView: React.FC = ({ ); const handleOnDragEnd = useCallback( - (result: DropResult) => { + async (result: DropResult) => { setTrashBox(false); if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return; @@ -281,7 +274,7 @@ export const IssuesView: React.FC = ({ ] ); - const addIssueToState = useCallback( + const addIssueToGroup = useCallback( (groupTitle: string) => { setCreateIssueModal(true); @@ -335,6 +328,15 @@ export const IssuesView: React.FC = ({ [setEditIssueModal, setIssueToEdit] ); + const handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + const removeIssueFromCycle = useCallback( (bridgeId: string, issueId: string) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -421,13 +423,6 @@ export const IssuesView: React.FC = ({ [workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert] ); - const handleTrashBox = useCallback( - (isDragging: boolean) => { - if (isDragging && !trashBox) setTrashBox(true); - }, - [trashBox, setTrashBox] - ); - const nullFilters = Object.keys(filters).filter( (key) => filters[key as keyof IIssueFilterOptions] === null ); @@ -461,14 +456,27 @@ export const IssuesView: React.FC = ({ data={issueToDelete} user={user} /> - setTransferIssuesModal(false)} - isOpen={transferIssuesModal} - /> {areFiltersApplied && ( <>
- + m.member)} + states={states} + clearAllFilters={() => + setFilters({ + assignees: null, + created_by: null, + labels: null, + priority: null, + state: null, + target_date: null, + type: null, + }) + } + /> { if (viewId) { @@ -492,140 +500,32 @@ export const IssuesView: React.FC = ({ {
} )} - - - - {(provided, snapshot) => ( -
- - Drop here to delete the issue. -
- )} -
- {groupedByIssues ? ( - !isEmpty || issueView === "kanban" || issueView === "calendar" ? ( - <> - {isCompleted && setTransferIssuesModal(true)} />} - {issueView === "list" ? ( - - ) : issueView === "kanban" ? ( - - ) : issueView === "calendar" ? ( - - ) : issueView === "spreadsheet" ? ( - - ) : ( - issueView === "gantt_chart" && - )} - - ) : router.pathname.includes("archived-issues") ? ( - { - router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`); - }} - /> - ) : ( - } - secondaryButton={ - cycleId || moduleId ? ( - - - Add an existing issue - - ) : null - } - onClick={() => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }} - /> - ) - ) : ( -
- -
- )} -
+ ); }; diff --git a/apps/app/components/core/views/list-view/all-lists.tsx b/apps/app/components/core/views/list-view/all-lists.tsx new file mode 100644 index 000000000..64cbebdcd --- /dev/null +++ b/apps/app/components/core/views/list-view/all-lists.tsx @@ -0,0 +1,62 @@ +// components +import { SingleList } from "components/core/views/list-view/single-list"; +// types +import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types"; + +// types +type Props = { + states: IState[] | undefined; + addIssueToGroup: (groupTitle: string) => void; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; + openIssuesListModal?: (() => void) | null; + removeIssue: ((bridgeId: string, issueId: string) => void) | null; + disableUserActions: boolean; + user: ICurrentUserResponse | undefined; + userAuth: UserAuth; + viewProps: IIssueViewProps; +}; + +export const AllLists: React.FC = ({ + addIssueToGroup, + handleIssueAction, + disableUserActions, + openIssuesListModal, + removeIssue, + states, + user, + userAuth, + viewProps, +}) => { + const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps; + + return ( + <> + {groupedIssues && ( +
+ {Object.keys(groupedIssues).map((singleGroup) => { + const currentState = + selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; + + if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null; + + return ( + addIssueToGroup(singleGroup)} + handleIssueAction={handleIssueAction} + openIssuesListModal={openIssuesListModal} + removeIssue={removeIssue} + disableUserActions={disableUserActions} + user={user} + userAuth={userAuth} + viewProps={viewProps} + /> + ); + })} +
+ )} + + ); +}; diff --git a/apps/app/components/core/list-view/index.ts b/apps/app/components/core/views/list-view/index.ts similarity index 100% rename from apps/app/components/core/list-view/index.ts rename to apps/app/components/core/views/list-view/index.ts diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/views/list-view/single-issue.tsx similarity index 93% rename from apps/app/components/core/list-view/single-issue.tsx rename to apps/app/components/core/views/list-view/single-issue.tsx index 77b67945d..11d3c0c75 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/views/list-view/single-issue.tsx @@ -18,8 +18,6 @@ import { ViewPrioritySelect, ViewStateSelect, } from "components/issues"; -// hooks -import useIssueView from "hooks/use-issues-view"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons @@ -37,7 +35,14 @@ import { LayerDiagonalIcon } from "components/icons"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { handleIssuesMutation } from "constants/issue"; // types -import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + IIssueViewProps, + ISubIssueResponse, + Properties, + UserAuth, +} from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -46,37 +51,38 @@ import { MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, SUB_ISSUES, + USER_ISSUES, VIEW_ISSUES, } from "constants/fetch-keys"; type Props = { type?: string; issue: IIssue; - properties: Properties; groupTitle?: string; editIssue: () => void; index: number; makeIssueCopy: () => void; removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; - isCompleted?: boolean; + disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; + viewProps: IIssueViewProps; }; export const SingleListIssue: React.FC = ({ type, issue, - properties, editIssue, index, makeIssueCopy, removeIssue, groupTitle, handleDeleteIssue, - isCompleted = false, + disableUserActions, user, userAuth, + viewProps, }) => { // context menu const [contextMenu, setContextMenu] = useState(false); @@ -88,11 +94,11 @@ export const SingleListIssue: React.FC = ({ const { setToastAlert } = useToast(); - const { groupByProperty: selectedGroup, orderBy, params } = useIssueView(); + const { groupByProperty: selectedGroup, orderBy, params, properties } = viewProps; const partialUpdateIssue = useCallback( (formData: Partial, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !issue) return; const fetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) @@ -100,7 +106,9 @@ export const SingleListIssue: React.FC = ({ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : viewId ? VIEW_ISSUES(viewId.toString(), params) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); + : router.pathname.includes("my-issues") + ? USER_ISSUES(workspaceSlug.toString(), params) + : PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project.toString(), params); if (issue.parent) { mutate( @@ -145,7 +153,7 @@ export const SingleListIssue: React.FC = ({ } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) + .patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user) .then(() => { mutate(fetchKey); @@ -155,7 +163,6 @@ export const SingleListIssue: React.FC = ({ }, [ workspaceSlug, - projectId, cycleId, moduleId, viewId, @@ -164,6 +171,7 @@ export const SingleListIssue: React.FC = ({ selectedGroup, orderBy, params, + router, user, ] ); @@ -186,7 +194,8 @@ export const SingleListIssue: React.FC = ({ ? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}` : `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted || isArchivedIssues; + const isNotAllowed = + userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues; return ( <> diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/views/list-view/single-list.tsx similarity index 77% rename from apps/app/components/core/list-view/single-list.tsx rename to apps/app/components/core/views/list-view/single-list.tsx index a277a129a..f06a978a7 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/views/list-view/single-list.tsx @@ -8,7 +8,7 @@ import { Disclosure, Transition } from "@headlessui/react"; import issuesService from "services/issues.service"; import projectService from "services/project.service"; // hooks -import useIssuesProperties from "hooks/use-issue-properties"; +import useProjects from "hooks/use-projects"; // components import { SingleListIssue } from "components/core"; // ui @@ -18,58 +18,52 @@ import { PlusIcon } from "@heroicons/react/24/outline"; import { getPriorityIcon, getStateGroupIcon } from "components/icons"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; +import { renderEmoji } from "helpers/emoji.helper"; // types import { ICurrentUserResponse, IIssue, IIssueLabels, + IIssueViewProps, IState, - TIssueGroupByOptions, UserAuth, } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; type Props = { - type?: "issue" | "cycle" | "module"; currentState?: IState | null; groupTitle: string; - groupedByIssues: { - [key: string]: IIssue[]; - }; - selectedGroup: TIssueGroupByOptions; - addIssueToState: () => void; - makeIssueCopy: (issue: IIssue) => void; - handleEditIssue: (issue: IIssue) => void; - handleDeleteIssue: (issue: IIssue) => void; + addIssueToGroup: () => void; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; openIssuesListModal?: (() => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null; - isCompleted?: boolean; + disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; + viewProps: IIssueViewProps; }; export const SingleList: React.FC = ({ - type, currentState, groupTitle, - groupedByIssues, - selectedGroup, - addIssueToState, - makeIssueCopy, - handleEditIssue, - handleDeleteIssue, + addIssueToGroup, + handleIssueAction, openIssuesListModal, removeIssue, - isCompleted = false, + disableUserActions, user, userAuth, + viewProps, }) => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const isArchivedIssues = router.pathname.includes("archived-issues"); - const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); + const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; + + const { groupByProperty: selectedGroup, groupedIssues } = viewProps; const { data: issueLabels } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, @@ -85,6 +79,8 @@ export const SingleList: React.FC = ({ : null ); + const { projects } = useProjects(); + const getGroupTitle = () => { let title = addSpaceIfCamelCase(groupTitle); @@ -95,6 +91,9 @@ export const SingleList: React.FC = ({ case "labels": title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None"; break; + case "project": + title = projects?.find((p) => p.id === groupTitle)?.name ?? "None"; + break; case "created_by": const member = members?.find((member) => member.member.id === groupTitle)?.member; title = @@ -115,9 +114,22 @@ export const SingleList: React.FC = ({ icon = currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); break; + case "state_detail.group": + icon = getStateGroupIcon(groupTitle as any, "16", "16"); + break; case "priority": icon = getPriorityIcon(groupTitle, "text-lg"); break; + case "project": + const project = projects?.find((p) => p.id === groupTitle); + icon = + project && + (project.emoji !== null + ? renderEmoji(project.emoji) + : project.icon_prop !== null + ? renderEmoji(project.icon_prop) + : null); + break; case "labels": const labelColor = issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000"; @@ -138,6 +150,8 @@ export const SingleList: React.FC = ({ return icon; }; + if (!groupedIssues) return null; + return ( {({ open }) => ( @@ -156,7 +170,7 @@ export const SingleList: React.FC = ({

All Issues

)} - {groupedByIssues[groupTitle as keyof IIssue].length} + {groupedIssues[groupTitle as keyof IIssue].length}
@@ -166,11 +180,11 @@ export const SingleList: React.FC = ({ - ) : isCompleted ? ( + ) : disableUserActions ? ( "" ) : ( = ({ position="right" noBorder > - Create new + Create new {openIssuesListModal && ( Add an existing issue @@ -201,26 +215,26 @@ export const SingleList: React.FC = ({ leaveTo="transform opacity-0" > - {groupedByIssues[groupTitle] ? ( - groupedByIssues[groupTitle].length > 0 ? ( - groupedByIssues[groupTitle].map((issue, index) => ( + {groupedIssues[groupTitle] ? ( + groupedIssues[groupTitle].length > 0 ? ( + groupedIssues[groupTitle].map((issue, index) => ( handleEditIssue(issue)} - makeIssueCopy={() => makeIssueCopy(issue)} - handleDeleteIssue={handleDeleteIssue} + editIssue={() => handleIssueAction(issue, "edit")} + makeIssueCopy={() => handleIssueAction(issue, "copy")} + handleDeleteIssue={() => handleIssueAction(issue, "delete")} removeIssue={() => { if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id); }} - isCompleted={isCompleted} + disableUserActions={disableUserActions} user={user} userAuth={userAuth} + viewProps={viewProps} /> )) ) : ( diff --git a/apps/app/components/core/spreadsheet-view/index.ts b/apps/app/components/core/views/spreadsheet-view/index.ts similarity index 100% rename from apps/app/components/core/spreadsheet-view/index.ts rename to apps/app/components/core/views/spreadsheet-view/index.ts diff --git a/apps/app/components/core/spreadsheet-view/single-issue.tsx b/apps/app/components/core/views/spreadsheet-view/single-issue.tsx similarity index 99% rename from apps/app/components/core/spreadsheet-view/single-issue.tsx rename to apps/app/components/core/views/spreadsheet-view/single-issue.tsx index bd2d5d101..888fb2d1e 100644 --- a/apps/app/components/core/spreadsheet-view/single-issue.tsx +++ b/apps/app/components/core/views/spreadsheet-view/single-issue.tsx @@ -53,7 +53,7 @@ type Props = { handleEditIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void; gridTemplateColumns: string; - isCompleted?: boolean; + disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; nestingLevel: number; @@ -68,7 +68,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ handleEditIssue, handleDeleteIssue, gridTemplateColumns, - isCompleted = false, + disableUserActions, user, userAuth, nestingLevel, @@ -190,7 +190,7 @@ export const SingleSpreadsheetIssue: React.FC = ({ {issue.project_detail?.identifier}-{issue.sequence_id} )} - {!isNotAllowed && !isCompleted && ( + {!isNotAllowed && !disableUserActions && (
>; properties: Properties; - handleEditIssue: (issue: IIssue) => void; - handleDeleteIssue: (issue: IIssue) => void; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; gridTemplateColumns: string; - isCompleted?: boolean; + disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; nestingLevel?: number; }; export const SpreadsheetIssues: React.FC = ({ - key, index, issue, expandedIssues, setExpandedIssues, gridTemplateColumns, properties, - handleEditIssue, - handleDeleteIssue, - isCompleted = false, + handleIssueAction, + disableUserActions, user, userAuth, nestingLevel = 0, @@ -64,9 +60,9 @@ export const SpreadsheetIssues: React.FC = ({ handleToggleExpand={handleToggleExpand} gridTemplateColumns={gridTemplateColumns} properties={properties} - handleEditIssue={handleEditIssue} - handleDeleteIssue={handleDeleteIssue} - isCompleted={isCompleted} + handleEditIssue={() => handleIssueAction(issue, "edit")} + handleDeleteIssue={() => handleIssueAction(issue, "delete")} + disableUserActions={disableUserActions} user={user} userAuth={userAuth} nestingLevel={nestingLevel} @@ -76,7 +72,7 @@ export const SpreadsheetIssues: React.FC = ({ !isLoading && subIssues && subIssues.length > 0 && - subIssues.map((subIssue: IIssue, subIndex: number) => ( + subIssues.map((subIssue: IIssue) => ( = ({ setExpandedIssues={setExpandedIssues} gridTemplateColumns={gridTemplateColumns} properties={properties} - handleEditIssue={handleEditIssue} - handleDeleteIssue={handleDeleteIssue} - isCompleted={isCompleted} + handleIssueAction={handleIssueAction} + disableUserActions={disableUserActions} user={user} userAuth={userAuth} nestingLevel={nestingLevel + 1} diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx b/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx similarity index 91% rename from apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx rename to apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 95bd96417..a4f426a23 100644 --- a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx +++ b/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -17,28 +17,26 @@ import { SPREADSHEET_COLUMN } from "constants/spreadsheet"; import { PlusIcon } from "@heroicons/react/24/outline"; type Props = { - type: "issue" | "cycle" | "module"; - handleEditIssue: (issue: IIssue) => void; - handleDeleteIssue: (issue: IIssue) => void; + handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; openIssuesListModal?: (() => void) | null; - isCompleted?: boolean; + disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; export const SpreadsheetView: React.FC = ({ - type, - handleEditIssue, - handleDeleteIssue, + handleIssueAction, openIssuesListModal, - isCompleted = false, + disableUserActions, user, userAuth, }) => { const [expandedIssues, setExpandedIssues] = useState([]); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + + const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; const { spreadsheetIssues } = useSpreadsheetIssuesView(); @@ -76,9 +74,8 @@ export const SpreadsheetView: React.FC = ({ setExpandedIssues={setExpandedIssues} gridTemplateColumns={gridTemplateColumns} properties={properties} - handleEditIssue={handleEditIssue} - handleDeleteIssue={handleDeleteIssue} - isCompleted={isCompleted} + handleIssueAction={handleIssueAction} + disableUserActions={disableUserActions} user={user} userAuth={userAuth} /> @@ -99,7 +96,7 @@ export const SpreadsheetView: React.FC = ({ Add Issue ) : ( - !isCompleted && ( + !disableUserActions && ( { switch (stateGroup) { case "backlog": - return ; + return ( + + ); case "unstarted": - return ; + return ( + + ); case "started": - return ; + return ( + + ); case "completed": - return ; + return ( + + ); case "cancelled": - return ; + return ( + + ); default: return <>; } diff --git a/apps/app/components/inbox/filters-dropdown.tsx b/apps/app/components/inbox/filters-dropdown.tsx index 308f0bc64..7bb601949 100644 --- a/apps/app/components/inbox/filters-dropdown.tsx +++ b/apps/app/components/inbox/filters-dropdown.tsx @@ -37,37 +37,35 @@ export const FiltersDropdown: React.FC = () => { id: "priority", label: "Priority", value: PRIORITIES, - children: [ - ...PRIORITIES.map((priority) => ({ - id: priority === null ? "null" : priority, - label: ( -
- {getPriorityIcon(priority)} {priority ?? "None"} -
- ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - ], + hasChildren: true, + children: PRIORITIES.map((priority) => ({ + id: priority === null ? "null" : priority, + label: ( +
+ {getPriorityIcon(priority)} {priority ?? "None"} +
+ ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), }, { id: "inbox_status", label: "Status", value: INBOX_STATUS.map((status) => status.value), - children: [ - ...INBOX_STATUS.map((status) => ({ - id: status.key, - label: status.label, - value: { - key: "inbox_status", - value: status.value, - }, - selected: filters?.inbox_status?.includes(status.value), - })), - ], + hasChildren: true, + children: INBOX_STATUS.map((status) => ({ + id: status.key, + label: status.label, + value: { + key: "inbox_status", + value: status.value, + }, + selected: filters?.inbox_status?.includes(status.value), + })), }, ]} /> diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx index a63d0f369..9a4725ae2 100644 --- a/apps/app/components/issues/delete-issue-modal.tsx +++ b/apps/app/components/issues/delete-issue-modal.tsx @@ -63,7 +63,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u if (!workspaceSlug || !projectId || !data) return; await issueServices - .deleteIssue(workspaceSlug as string, projectId as string, data.id, user) + .deleteIssue(workspaceSlug as string, data.project, data.id, user) .then(() => { if (issueView === "calendar") { const calendarFetchKey = cycleId @@ -72,7 +72,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) : viewId ? VIEW_ISSUES(viewId.toString(), calendarParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), calendarParams); + : PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, calendarParams); mutate( calendarFetchKey, @@ -86,7 +86,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) : viewId ? VIEW_ISSUES(viewId.toString(), spreadsheetParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams); + : PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, spreadsheetParams); if (data.parent) { mutate( SUB_ISSUES(data.parent.toString()), diff --git a/apps/app/components/issues/index.ts b/apps/app/components/issues/index.ts index 7e9bff175..ea8cbc603 100644 --- a/apps/app/components/issues/index.ts +++ b/apps/app/components/issues/index.ts @@ -1,5 +1,6 @@ export * from "./attachment"; export * from "./comment"; +export * from "./my-issues"; export * from "./sidebar-select"; export * from "./view-select"; export * from "./activity"; @@ -9,7 +10,6 @@ export * from "./form"; export * from "./gantt-chart"; export * from "./main-content"; export * from "./modal"; -export * from "./my-issues-list-item"; export * from "./parent-issues-list-modal"; export * from "./sidebar"; export * from "./sub-issues-list"; diff --git a/apps/app/components/issues/my-issues-list-item.tsx b/apps/app/components/issues/my-issues-list-item.tsx deleted file mode 100644 index e4eb4f82a..000000000 --- a/apps/app/components/issues/my-issues-list-item.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React, { useCallback } from "react"; - -import Link from "next/link"; -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// hooks -import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; -// services -import issuesService from "services/issues.service"; -// components -import { - ViewDueDateSelect, - ViewPrioritySelect, - ViewStateSelect, -} from "components/issues/view-select"; -// icon -import { LinkIcon, PaperClipIcon } from "@heroicons/react/24/outline"; -import { LayerDiagonalIcon } from "components/icons"; -// ui -import { AssigneesList } from "components/ui/avatar"; -import { CustomMenu, Tooltip } from "components/ui"; -// types -import { IIssue, Properties } from "types"; -// helper -import { copyTextToClipboard, truncateText } from "helpers/string.helper"; -// fetch-keys -import { USER_ISSUE } from "constants/fetch-keys"; - -type Props = { - issue: IIssue; - properties: Properties; - projectId: string; -}; - -export const MyIssuesListItem: React.FC = ({ issue, properties, projectId }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { user } = useUserAuth(); - - const { setToastAlert } = useToast(); - - const partialUpdateIssue = useCallback( - (formData: Partial, issue: IIssue) => { - if (!workspaceSlug) return; - - mutate( - USER_ISSUE(workspaceSlug as string), - (prevData) => - prevData?.map((p) => { - if (p.id === issue.id) return { ...p, ...formData }; - - return p; - }), - false - ); - - issuesService - .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user) - .then((res) => { - mutate(USER_ISSUE(workspaceSlug as string)); - }) - .catch((error) => { - console.log(error); - }); - }, - [workspaceSlug, projectId, user] - ); - - const handleCopyText = () => { - const originURL = - typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard( - `${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}` - ).then(() => { - setToastAlert({ - type: "success", - title: "Link Copied!", - message: "Issue link copied to clipboard.", - }); - }); - }; - - const isNotAllowed = false; - - return ( -
-
- - - {properties?.key && ( - - - {issue.project_detail?.identifier}-{issue.sequence_id} - - - )} - - - {truncateText(issue.name, 50)} - - - - - -
- {properties.priority && ( - - )} - {properties.state && ( - - )} - {properties.due_date && issue.target_date && ( - - )} - {properties.labels && issue.labels.length > 0 && ( -
- {issue.label_details.map((label) => ( - - - {label.name} - - ))} -
- )} - {properties.assignee && ( -
- 0 - ? issue.assignee_details - .map((assignee) => - assignee?.first_name !== "" ? assignee?.first_name : assignee?.email - ) - .join(", ") - : "No Assignee" - } - > -
- -
-
-
- )} - {properties.sub_issue_count && issue.sub_issues_count > 0 && ( -
- -
- - {issue.sub_issues_count} -
-
-
- )} - {properties.link && issue.link_count > 0 && ( -
- -
- - {issue.link_count} -
-
-
- )} - {properties.attachment_count && issue.attachment_count > 0 && ( -
- -
- - {issue.attachment_count} -
-
-
- )} - - - - - Copy issue link - - - -
-
-
- ); -}; diff --git a/apps/app/components/issues/my-issues/index.ts b/apps/app/components/issues/my-issues/index.ts new file mode 100644 index 000000000..65a063f4c --- /dev/null +++ b/apps/app/components/issues/my-issues/index.ts @@ -0,0 +1,3 @@ +export * from "./my-issues-select-filters"; +export * from "./my-issues-view-options"; +export * from "./my-issues-view"; diff --git a/apps/app/components/issues/my-issues/my-issues-select-filters.tsx b/apps/app/components/issues/my-issues/my-issues-select-filters.tsx new file mode 100644 index 000000000..e3d2cdff0 --- /dev/null +++ b/apps/app/components/issues/my-issues/my-issues-select-filters.tsx @@ -0,0 +1,168 @@ +import { useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import issuesService from "services/issues.service"; +// components +import { DueDateFilterModal } from "components/core"; +// ui +import { MultiLevelDropdown } from "components/ui"; +// icons +import { getPriorityIcon, getStateGroupIcon } from "components/icons"; +// helpers +import { checkIfArraysHaveSameElements } from "helpers/array.helper"; +// types +import { IIssueFilterOptions, IQuery } from "types"; +// fetch-keys +import { WORKSPACE_LABELS } from "constants/fetch-keys"; +// constants +import { GROUP_CHOICES, PRIORITIES } from "constants/project"; +import { DUE_DATES } from "constants/due-dates"; + +type Props = { + filters: Partial | IQuery; + onSelect: (option: any) => void; + direction?: "left" | "right"; + height?: "sm" | "md" | "rg" | "lg"; +}; + +export const MyIssuesSelectFilters: React.FC = ({ + filters, + onSelect, + direction = "right", + height = "md", +}) => { + const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false); + const [fetchLabels, setFetchLabels] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { data: labels } = useSWR( + workspaceSlug && fetchLabels ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, + workspaceSlug && fetchLabels + ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) + : null + ); + + return ( + <> + {isDueDateFilterModalOpen && ( + setIsDueDateFilterModalOpen(false)} + /> + )} + ({ + id: priority === null ? "null" : priority, + label: ( +
+ {getPriorityIcon(priority)} {priority ?? "None"} +
+ ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), + ], + }, + { + id: "state_group", + label: "State groups", + value: GROUP_CHOICES, + hasChildren: true, + children: [ + ...Object.keys(GROUP_CHOICES).map((key) => ({ + id: key, + label: ( +
+ {getStateGroupIcon(key as any, "16", "16")}{" "} + {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} +
+ ), + value: { + key: "state_group", + value: key, + }, + selected: filters?.state?.includes(key), + })), + ], + }, + { + id: "labels", + label: "Labels", + onClick: () => setFetchLabels(true), + value: labels, + hasChildren: true, + children: labels?.map((label) => ({ + id: label.id, + label: ( +
+
+ {label.name} +
+ ), + value: { + key: "labels", + value: label.id, + }, + selected: filters?.labels?.includes(label.id), + })), + }, + { + id: "target_date", + label: "Due date", + value: DUE_DATES, + hasChildren: true, + children: [ + ...(DUE_DATES?.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: ( + + ), + }, + ], + }, + ]} + /> + + ); +}; diff --git a/apps/app/components/issues/my-issues/my-issues-view-options.tsx b/apps/app/components/issues/my-issues/my-issues-view-options.tsx new file mode 100644 index 000000000..24d59fa03 --- /dev/null +++ b/apps/app/components/issues/my-issues/my-issues-view-options.tsx @@ -0,0 +1,290 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +// headless ui +import { Popover, Transition } from "@headlessui/react"; +// hooks +import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; +import useEstimateOption from "hooks/use-estimate-option"; +// components +import { MyIssuesSelectFilters } from "components/issues"; +// ui +import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui"; +// icons +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material"; +// helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +import { checkIfArraysHaveSameElements } from "helpers/array.helper"; +// types +import { Properties, TIssueViewOptions } from "types"; +// constants +import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; + +const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ + { + type: "list", + Icon: FormatListBulletedOutlined, + }, + { + type: "kanban", + Icon: GridViewOutlined, + }, +]; + +export const MyIssuesViewOptions: React.FC = () => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { + issueView, + setIssueView, + groupBy, + setGroupBy, + orderBy, + setOrderBy, + showEmptyGroups, + setShowEmptyGroups, + properties, + setProperty, + filters, + setFilters, + } = useMyIssuesFilters(workspaceSlug?.toString()); + + const { isEstimateActive } = useEstimateOption(); + + return ( +
+
+ {issueViewOptions.map((option) => ( + {replaceUnderscoreIfSnakeCase(option.type)} View + } + position="bottom" + > + + + ))} +
+ { + const key = option.key as keyof typeof filters; + + if (key === "target_date") { + const valueExists = checkIfArraysHaveSameElements( + filters?.target_date ?? [], + option.value + ); + + setFilters({ + target_date: valueExists ? null : option.value, + }); + } else { + const valueExists = filters[key]?.includes(option.value); + + if (valueExists) + setFilters({ + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }); + else + setFilters({ + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }); + } + }} + direction="left" + height="rg" + /> + + {({ open }) => ( + <> + + View + + + + +
+
+ {issueView !== "calendar" && issueView !== "spreadsheet" && ( + <> +
+

Group by

+ option.key === groupBy)?.name ?? + "Select" + } + > + {GROUP_BY_OPTIONS.map((option) => { + if (issueView === "kanban" && option.key === null) return null; + if (option.key === "state" || option.key === "created_by") + return null; + + return ( + setGroupBy(option.key)} + > + {option.name} + + ); + })} + +
+
+

Order by

+ option.key === orderBy)?.name ?? + "Select" + } + > + {ORDER_BY_OPTIONS.map((option) => { + if (groupBy === "priority" && option.key === "priority") return null; + if (option.key === "sort_order") return null; + + return ( + { + setOrderBy(option.key); + }} + > + {option.name} + + ); + })} + +
+ + )} +
+

Issue type

+ option.key === filters?.type) + ?.name ?? "Select" + } + > + {FILTER_ISSUE_OPTIONS.map((option) => ( + + setFilters({ + type: option.key, + }) + } + > + {option.name} + + ))} + +
+ + {issueView !== "calendar" && issueView !== "spreadsheet" && ( + <> +
+

Show empty states

+ +
+ {/*
+ + +
*/} + + )} +
+ +
+

Display Properties

+
+ {Object.keys(properties).map((key) => { + if (key === "estimate" && !isEstimateActive) return null; + + if ( + issueView === "spreadsheet" && + (key === "attachment_count" || + key === "link" || + key === "sub_issue_count") + ) + return null; + + if ( + issueView !== "spreadsheet" && + (key === "created_on" || key === "updated_on") + ) + return null; + + return ( + + ); + })} +
+
+
+
+
+ + )} +
+
+ ); +}; diff --git a/apps/app/components/issues/my-issues/my-issues-view.tsx b/apps/app/components/issues/my-issues/my-issues-view.tsx new file mode 100644 index 000000000..6e9655c2a --- /dev/null +++ b/apps/app/components/issues/my-issues/my-issues-view.tsx @@ -0,0 +1,288 @@ +import { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// react-beautiful-dnd +import { DropResult } from "react-beautiful-dnd"; +// services +import issuesService from "services/issues.service"; +// hooks +import useMyIssues from "hooks/my-issues/use-my-issues"; +import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; +import useUserAuth from "hooks/use-user-auth"; +// components +import { AllViews, FiltersList } from "components/core"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { CreateUpdateViewModal } from "components/views"; +// types +import { IIssue, IIssueFilterOptions } from "types"; +// fetch-keys +import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys"; +import { orderArrayBy } from "helpers/array.helper"; + +type Props = { + openIssuesListModal?: () => void; + disableUserActions?: false; +}; + +export const MyIssuesView: React.FC = ({ + openIssuesListModal, + disableUserActions = false, +}) => { + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [createViewModal, setCreateViewModal] = useState(null); + 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); + + // trash box + const [trashBox, setTrashBox] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { user } = useUserAuth(); + + const { groupedIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString()); + const { filters, setFilters, issueView, groupBy, orderBy, properties, showEmptyGroups } = + useMyIssuesFilters(workspaceSlug?.toString()); + + const { data: labels } = useSWR( + workspaceSlug && (filters.labels ?? []).length > 0 + ? WORKSPACE_LABELS(workspaceSlug.toString()) + : null, + workspaceSlug && (filters.labels ?? []).length > 0 + ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) + : null + ); + + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + + const handleOnDragEnd = useCallback( + async (result: DropResult) => { + setTrashBox(false); + console.log(result); + + if (!result.destination || !workspaceSlug || !groupedIssues || groupBy !== "priority") return; + + const { source, destination } = result; + + if (source.droppableId === destination.droppableId) return; + + const draggedItem = groupedIssues[source.droppableId][source.index]; + + if (!draggedItem) return; + + if (destination.droppableId === "trashBox") handleDeleteIssue(draggedItem); + else { + const sourceGroup = source.droppableId; + const destinationGroup = destination.droppableId; + + draggedItem[groupBy] = destinationGroup; + + mutate<{ + [key: string]: IIssue[]; + }>( + USER_ISSUES(workspaceSlug.toString(), params), + (prevData) => { + if (!prevData) return prevData; + + const sourceGroupArray = [...groupedIssues[sourceGroup]]; + const destinationGroupArray = [...groupedIssues[destinationGroup]]; + + sourceGroupArray.splice(source.index, 1); + destinationGroupArray.splice(destination.index, 0, draggedItem); + + return { + ...prevData, + [sourceGroup]: orderArrayBy(sourceGroupArray, orderBy), + [destinationGroup]: orderArrayBy(destinationGroupArray, orderBy), + }; + }, + false + ); + + // patch request + issuesService + .patchIssue( + workspaceSlug as string, + draggedItem.project, + draggedItem.id, + { + priority: draggedItem.priority, + }, + user + ) + .catch(() => mutate(USER_ISSUES(workspaceSlug.toString(), params))); + } + }, + [groupBy, groupedIssues, handleDeleteIssue, orderBy, params, user, workspaceSlug] + ); + + const addIssueToGroup = useCallback( + (groupTitle: string) => { + setCreateIssueModal(true); + + let preloadedValue: string | string[] = groupTitle; + + if (groupBy === "labels") { + if (groupTitle === "None") preloadedValue = []; + else preloadedValue = [groupTitle]; + } + + if (groupBy) + setPreloadedData({ + [groupBy]: preloadedValue, + actionType: "createIssue", + }); + else setPreloadedData({ actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData, groupBy] + ); + + const addIssueToDate = useCallback( + (date: string) => { + setCreateIssueModal(true); + setPreloadedData({ + target_date: date, + actionType: "createIssue", + }); + }, + [setCreateIssueModal, setPreloadedData] + ); + + 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 handleIssueAction = useCallback( + (issue: IIssue, action: "copy" | "edit" | "delete") => { + if (action === "copy") makeIssueCopy(issue); + else if (action === "edit") handleEditIssue(issue); + else if (action === "delete") handleDeleteIssue(issue); + }, + [makeIssueCopy, handleEditIssue, handleDeleteIssue] + ); + const filtersToShow = { ...filters }; + delete filtersToShow?.assignees; + delete filtersToShow?.created_by; + + const nullFilters = Object.keys(filtersToShow).filter( + (key) => filtersToShow[key as keyof IIssueFilterOptions] === null + ); + + const areFiltersApplied = + Object.keys(filtersToShow).length > 0 && + nullFilters.length !== Object.keys(filtersToShow).length; + + return ( + <> + setCreateViewModal(null)} + preLoadedData={createViewModal} + user={user} + /> + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + user={user} + /> + {areFiltersApplied && ( + <> +
+ + setFilters({ + labels: null, + priority: null, + state_group: null, + target_date: null, + type: null, + }) + } + /> +
+ {
} + + )} + + + ); +}; diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 6f679fe7c..2de7fe52a 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -1,3 +1,5 @@ +import { useState } from "react"; + import { useRouter } from "next/router"; import useSWR from "swr"; @@ -39,12 +41,14 @@ export const ViewStateSelect: React.FC = ({ user, isNotAllowed, }) => { + const [fetchStates, setFetchStates] = useState(false); + const router = useRouter(); const { workspaceSlug } = router.query; const { data: stateGroups } = useSWR( - workspaceSlug && issue ? STATES_LIST(issue.project) : null, - workspaceSlug && issue + workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null, + workspaceSlug && issue && fetchStates ? () => stateService.getStates(workspaceSlug as string, issue.project) : null ); @@ -61,7 +65,7 @@ export const ViewStateSelect: React.FC = ({ ), })); - const selectedOption = states?.find((s) => s.id === issue.state); + const selectedOption = issue.state_detail; const stateLabel = ( = ({ {...(customButton ? { customButton: stateLabel } : { label: stateLabel })} position={position} disabled={isNotAllowed} + onOpen={() => setFetchStates(true)} noChevron /> ); diff --git a/apps/app/components/project/create-project-modal.tsx b/apps/app/components/project/create-project-modal.tsx index f95d06ce0..6cdf1dc48 100644 --- a/apps/app/components/project/create-project-modal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -244,20 +244,7 @@ export const CreateProjectModal: React.FC = ({ isOpen, setIsOpen, user }) - {value ? ( - typeof value === "object" ? ( - - {value.name} - - ) : ( - renderEmoji(value) - ) - ) : ( - "Icon" - )} + {value ? renderEmoji(value) : "Icon"}
} onChange={onChange} diff --git a/apps/app/components/project/single-project-card.tsx b/apps/app/components/project/single-project-card.tsx index 998e6f030..175e7430c 100644 --- a/apps/app/components/project/single-project-card.tsx +++ b/apps/app/components/project/single-project-card.tsx @@ -176,12 +176,7 @@ export const SingleProjectCard: React.FC = ({ {renderEmoji(project.emoji)} ) : project.icon_prop ? ( - - {project.icon_prop.name} - + renderEmoji(project.icon_prop) ) : null}

diff --git a/apps/app/components/project/single-sidebar-project.tsx b/apps/app/components/project/single-sidebar-project.tsx index e43ff0a65..8aaba64da 100644 --- a/apps/app/components/project/single-sidebar-project.tsx +++ b/apps/app/components/project/single-sidebar-project.tsx @@ -150,12 +150,7 @@ export const SingleSidebarProject: React.FC = ({ ) : project.icon_prop ? (

- - {project.icon_prop.name} - + {renderEmoji(project.icon_prop)}
) : ( diff --git a/apps/app/components/ui/dropdowns/custom-search-select.tsx b/apps/app/components/ui/dropdowns/custom-search-select.tsx index 2afa5bd72..e40976d13 100644 --- a/apps/app/components/ui/dropdowns/custom-search-select.tsx +++ b/apps/app/components/ui/dropdowns/custom-search-select.tsx @@ -39,6 +39,7 @@ export const CustomSearchSelect = ({ noChevron = false, onChange, options, + onOpen, optionsClassName = "", position = "left", selfPositioned = false, @@ -67,115 +68,119 @@ export const CustomSearchSelect = ({ className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`} {...props} > - {({ open }: any) => ( - <> - {customButton ? ( - {customButton} - ) : ( - - {label} - {!noChevron && !disabled && ( - - )} - - -
- - setQuery(e.target.value)} - placeholder="Type to search..." - displayValue={(assigned: any) => assigned?.name} - /> -
-
{ + if (open && onOpen) onOpen(); + + return ( + <> + {customButton ? ( + {customButton} + ) : ( + - {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || selected ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ active, selected }) => ( - <> - {option.content} - {multiple ? ( -
+ {label} + {!noChevron && !disabled && ( +
- {footerOption} - - - - )} +

Loading...

+ )} +
+ {footerOption} +
+
+ + ); + }} ); }; diff --git a/apps/app/components/ui/dropdowns/types.d.ts b/apps/app/components/ui/dropdowns/types.d.ts index b165d1969..aace1858a 100644 --- a/apps/app/components/ui/dropdowns/types.d.ts +++ b/apps/app/components/ui/dropdowns/types.d.ts @@ -7,6 +7,7 @@ export type DropdownProps = { label?: string | JSX.Element; maxHeight?: "sm" | "rg" | "md" | "lg"; noChevron?: boolean; + onOpen?: () => void; optionsClassName?: string; position?: "right" | "left"; selfPositioned?: boolean; diff --git a/apps/app/components/ui/multi-level-dropdown.tsx b/apps/app/components/ui/multi-level-dropdown.tsx index 8cdda00d4..9a4ebdbe4 100644 --- a/apps/app/components/ui/multi-level-dropdown.tsx +++ b/apps/app/components/ui/multi-level-dropdown.tsx @@ -2,6 +2,8 @@ import { Fragment, useState } from "react"; // headless ui import { Menu, Transition } from "@headlessui/react"; +// ui +import { Loader } from "components/ui"; // icons import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid"; @@ -10,9 +12,6 @@ type MultiLevelDropdownProps = { label: string; options: { id: string; - label: string; - value: any; - selected?: boolean; children?: { id: string; label: string | JSX.Element; @@ -20,6 +19,11 @@ type MultiLevelDropdownProps = { selected?: boolean; element?: JSX.Element; }[]; + hasChildren: boolean; + label: string; + onClick?: () => void; + selected?: boolean; + value: any; }[]; onSelect: (value: any) => void; direction?: "left" | "right"; @@ -69,15 +73,15 @@ export const MultiLevelDropdown: React.FC = ({ { - if (option.children) { + if (option.hasChildren) { e.stopPropagation(); e.preventDefault(); + if (option.onClick) option.onClick(); + if (openChildFor === option.id) setOpenChildFor(null); else setOpenChildFor(option.id); - } else { - onSelect(option.value); - } + } else onSelect(option.value); }} className="w-full" > @@ -90,18 +94,18 @@ export const MultiLevelDropdown: React.FC = ({ direction === "right" ? "justify-between" : "" }`} > - {direction === "left" && option.children && ( + {direction === "left" && option.hasChildren && (
)} - {option.children && option.id === openChildFor && ( + {option.hasChildren && option.id === openChildFor && (
= ({ : "" }`} > -
- {option.children.map((child) => { - if (child.element) return child.element; - else - return ( - - ); - })} -
+ {option.children ? ( +
+ {option.children.map((child) => { + if (child.element) return child.element; + else + return ( + + ); + })} +
+ ) : ( + + + + + + + )}
)}
diff --git a/apps/app/components/views/form.tsx b/apps/app/components/views/form.tsx index dc1cf9fba..ec6fadd0a 100644 --- a/apps/app/components/views/form.tsx +++ b/apps/app/components/views/form.tsx @@ -1,14 +1,28 @@ import { useEffect } from "react"; +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// react-hook-form import { useForm } from "react-hook-form"; +// services +import stateService from "services/state.service"; +// hooks +import useProjectMembers from "hooks/use-project-members"; +// components +import { FiltersList } from "components/core"; +import { SelectFilters } from "components/views"; // ui import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -// components -import { FilterList } from "components/core"; +// helpers +import { checkIfArraysHaveSameElements } from "helpers/array.helper"; +import { getStatesList } from "helpers/state.helper"; // types -import { IView } from "types"; -// components -import { SelectFilters } from "components/views"; +import { IQuery, IView } from "types"; +import issuesService from "services/issues.service"; +// fetch-keys +import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys"; type Props = { handleFormSubmit: (values: IView) => Promise; @@ -30,6 +44,9 @@ export const ViewForm: React.FC = ({ data, preLoadedData, }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + const { register, formState: { errors, isSubmitting }, @@ -42,6 +59,26 @@ export const ViewForm: React.FC = ({ }); const filters = watch("query"); + const { data: stateGroups } = useSWR( + workspaceSlug && projectId && (filters.state ?? []).length > 0 + ? STATES_LIST(projectId as string) + : null, + workspaceSlug && (filters.state ?? []).length > 0 + ? () => stateService.getStates(workspaceSlug as string, projectId as string) + : null + ); + const states = getStatesList(stateGroups ?? {}); + + const { data: labels } = useSWR( + workspaceSlug && projectId && (filters.labels ?? []).length > 0 + ? PROJECT_ISSUE_LABELS(projectId.toString()) + : null, + workspaceSlug && projectId && (filters.labels ?? []).length > 0 + ? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString()) + : null + ); + const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString()); + const handleCreateUpdateView = async (formData: IView) => { await handleFormSubmit(formData); @@ -50,6 +87,18 @@ export const ViewForm: React.FC = ({ }); }; + const clearAllFilters = () => { + setValue("query", { + assignees: null, + created_by: null, + labels: null, + priority: null, + state: null, + target_date: null, + type: null, + }); + }; + useEffect(() => { reset({ ...defaultValues, @@ -106,23 +155,38 @@ export const ViewForm: React.FC = ({ onSelect={(option) => { const key = option.key as keyof typeof filters; - if (!filters?.[key]?.includes(option.value)) + if (key === "target_date") { + const valueExists = checkIfArraysHaveSameElements( + filters?.target_date ?? [], + option.value + ); + setValue("query", { - ...filters, - [key]: [...((filters?.[key] as any[]) ?? []), option.value], - }); - else { - setValue("query", { - ...filters, - [key]: (filters?.[key] as any[])?.filter((item) => item !== option.value), - }); + target_date: valueExists ? null : option.value, + } as IQuery); + } else { + if (!filters?.[key]?.includes(option.value)) + setValue("query", { + ...filters, + [key]: [...((filters?.[key] as any[]) ?? []), option.value], + }); + else { + setValue("query", { + ...filters, + [key]: (filters?.[key] as any[])?.filter((item) => item !== option.value), + }); + } } }} />
- m.member)} + states={states} + clearAllFilters={clearAllFilters} setFilters={(query: any) => { setValue("query", { ...filters, diff --git a/apps/app/components/views/select-filters.tsx b/apps/app/components/views/select-filters.tsx index bf7bf9cf9..c76060e13 100644 --- a/apps/app/components/views/select-filters.tsx +++ b/apps/app/components/views/select-filters.tsx @@ -83,121 +83,116 @@ export const SelectFilters: React.FC = ({ id: "priority", label: "Priority", value: PRIORITIES, - children: [ - ...PRIORITIES.map((priority) => ({ - id: priority === null ? "null" : priority, - label: ( -
- {getPriorityIcon(priority)} {priority ?? "None"} -
- ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - ], + hasChildren: true, + children: PRIORITIES.map((priority) => ({ + id: priority === null ? "null" : priority, + label: ( +
+ {getPriorityIcon(priority)} {priority ?? "None"} +
+ ), + value: { + key: "priority", + value: priority === null ? "null" : priority, + }, + selected: filters?.priority?.includes(priority === null ? "null" : priority), + })), }, { id: "state", label: "State", value: statesList, - children: [ - ...statesList.map((state) => ({ - id: state.id, - label: ( -
- {getStateGroupIcon(state.group, "16", "16", state.color)} {state.name} -
- ), - value: { - key: "state", - value: state.id, - }, - selected: filters?.state?.includes(state.id), - })), - ], + hasChildren: true, + children: statesList.map((state) => ({ + id: state.id, + label: ( +
+ {getStateGroupIcon(state.group, "16", "16", state.color)} {state.name} +
+ ), + value: { + key: "state", + value: state.id, + }, + selected: filters?.state?.includes(state.id), + })), }, { id: "assignees", label: "Assignees", value: members, - children: [ - ...(members?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name - : member.member.email} -
- ), - value: { - key: "assignees", - value: member.member.id, - }, - selected: filters?.assignees?.includes(member.member.id), - })) ?? []), - ], + hasChildren: true, + children: members?.map((member) => ({ + id: member.member.id, + label: ( +
+ + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} +
+ ), + value: { + key: "assignees", + value: member.member.id, + }, + selected: filters?.assignees?.includes(member.member.id), + })), }, { id: "created_by", label: "Created by", value: members, - children: [ - ...(members?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name - : member.member.email} -
- ), - value: { - key: "created_by", - value: member.member.id, - }, - selected: filters?.created_by?.includes(member.member.id), - })) ?? []), - ], + hasChildren: true, + children: members?.map((member) => ({ + id: member.member.id, + label: ( +
+ + {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} +
+ ), + value: { + key: "created_by", + value: member.member.id, + }, + selected: filters?.created_by?.includes(member.member.id), + })), }, { id: "labels", label: "Labels", value: issueLabels, - children: [ - ...(issueLabels?.map((label) => ({ - id: label.id, - label: ( -
-
- {label.name} -
- ), - value: { - key: "labels", - value: label.id, - }, - selected: filters?.labels?.includes(label.id), - })) ?? []), - ], + hasChildren: true, + children: issueLabels?.map((label) => ({ + id: label.id, + label: ( +
+
+ {label.name} +
+ ), + value: { + key: "labels", + value: label.id, + }, + selected: filters?.labels?.includes(label.id), + })), }, { id: "target_date", label: "Due date", value: DUE_DATES, + hasChildren: true, children: [ - ...(DUE_DATES?.map((option) => ({ + ...DUE_DATES.map((option) => ({ id: option.name, label: option.name, value: { @@ -205,7 +200,7 @@ export const SelectFilters: React.FC = ({ value: option.value, }, selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), - })) ?? []), + })), { id: "custom", label: "Custom", diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index f52ecc2e0..dd36817aa 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -37,6 +37,29 @@ const inboxParamsToKey = (params: any) => { return `${priorityKey}_${inboxStatusKey}`; }; +const myIssuesParamsToKey = (params: any) => { + const { assignees, created_by, labels, priority, state_group, target_date } = params; + + let assigneesKey = assignees ? assignees.split(",") : []; + let createdByKey = created_by ? created_by.split(",") : []; + let stateGroupKey = state_group ? state_group.split(",") : []; + let priorityKey = priority ? priority.split(",") : []; + let labelsKey = labels ? labels.split(",") : []; + const targetDateKey = target_date ?? ""; + const type = params.type ? params.type.toUpperCase() : "NULL"; + const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL"; + const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL"; + + // sorting each keys in ascending order + assigneesKey = assigneesKey.sort().join("_"); + createdByKey = createdByKey.sort().join("_"); + stateGroupKey = stateGroupKey.sort().join("_"); + priorityKey = priorityKey.sort().join("_"); + labelsKey = labelsKey.sort().join("_"); + + return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`; +}; + export const CURRENT_USER = "CURRENT_USER"; export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS"; export const USER_WORKSPACES = "USER_WORKSPACES"; @@ -97,6 +120,8 @@ export const PROJECT_ISSUE_BY_STATE = (projectId: string) => `PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`; export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`; +export const WORKSPACE_LABELS = (workspaceSlug: string) => + `WORKSPACE_LABELS_${workspaceSlug.toUpperCase()}`; export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`; @@ -123,9 +148,15 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`; export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`; -export const STATE_DETAILS = "STATE_DETAILS"; export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`; +export const USER_ISSUES = (workspaceSlug: string, params: any) => { + if (!params) return `USER_ISSUES_${workspaceSlug.toUpperCase()}`; + + const paramsKey = myIssuesParamsToKey(params); + + return `USER_ISSUES_${paramsKey}`; +}; export const USER_ACTIVITY = "USER_ACTIVITY"; export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`; diff --git a/apps/app/constants/issue.ts b/apps/app/constants/issue.ts index 0bf3f4ffb..3665f180c 100644 --- a/apps/app/constants/issue.ts +++ b/apps/app/constants/issue.ts @@ -2,8 +2,10 @@ export const GROUP_BY_OPTIONS: Array<{ name: string; key: TIssueGroupByOptions; }> = [ - { name: "State", key: "state" }, + { name: "States", key: "state" }, + { name: "State Groups", key: "state_detail.group" }, { name: "Priority", key: "priority" }, + { name: "Project", key: "project" }, { name: "Labels", key: "labels" }, { name: "Created by", key: "created_by" }, { name: "None", key: null }, diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index 8399b8558..e029c4203 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -91,8 +91,6 @@ export const initialState: StateType = { assignees: null, labels: null, state: null, - issue__assignees__id: null, - issue__labels__id: null, created_by: null, target_date: null, }, diff --git a/apps/app/helpers/emoji.helper.ts b/apps/app/helpers/emoji.helper.ts deleted file mode 100644 index f16d0021c..000000000 --- a/apps/app/helpers/emoji.helper.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const getRandomEmoji = () => { - const emojis = [ - "8986", - "9200", - "128204", - "127773", - "127891", - "127947", - "128076", - "128077", - "128187", - "128188", - "128512", - "128522", - "128578", - ]; - - return emojis[Math.floor(Math.random() * emojis.length)]; -}; - -export const renderEmoji = (emoji: string) => { - if (!emoji) return; - - return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); -}; diff --git a/apps/app/helpers/emoji.helper.tsx b/apps/app/helpers/emoji.helper.tsx new file mode 100644 index 000000000..7e4acd2ef --- /dev/null +++ b/apps/app/helpers/emoji.helper.tsx @@ -0,0 +1,38 @@ +export const getRandomEmoji = () => { + const emojis = [ + "8986", + "9200", + "128204", + "127773", + "127891", + "127947", + "128076", + "128077", + "128187", + "128188", + "128512", + "128522", + "128578", + ]; + + return emojis[Math.floor(Math.random() * emojis.length)]; +}; + +export const renderEmoji = ( + emoji: + | string + | { + name: string; + color: string; + } +) => { + if (!emoji) return; + + if (typeof emoji === "object") + return ( + + {emoji.name} + + ); + else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); +}; diff --git a/apps/app/hooks/my-issues/use-my-issues-filter.tsx b/apps/app/hooks/my-issues/use-my-issues-filter.tsx new file mode 100644 index 000000000..aef6191be --- /dev/null +++ b/apps/app/hooks/my-issues/use-my-issues-filter.tsx @@ -0,0 +1,201 @@ +import { useEffect, useCallback } from "react"; + +import useSWR, { mutate } from "swr"; + +// services +import workspaceService from "services/workspace.service"; +// types +import { + IIssueFilterOptions, + IWorkspaceMember, + IWorkspaceViewProps, + Properties, + TIssueGroupByOptions, + TIssueOrderByOptions, + TIssueViewOptions, +} from "types"; +// fetch-keys +import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; + +const initialValues: IWorkspaceViewProps = { + issueView: "list", + filters: { + assignees: null, + created_by: null, + labels: null, + priority: null, + state_group: null, + target_date: null, + type: null, + }, + groupByProperty: null, + orderBy: "-created_at", + properties: { + assignee: 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, + }, + showEmptyGroups: true, +}; + +const useMyIssuesFilters = (workspaceSlug: string | undefined) => { + const { data: myWorkspace } = useSWR( + workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null, + workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug as string) : null, + { + shouldRetryOnError: false, + } + ); + + const saveData = useCallback( + (data: Partial) => { + if (!workspaceSlug || !myWorkspace) return; + + const oldData = { ...myWorkspace }; + + mutate( + WORKSPACE_MEMBERS_ME(workspaceSlug.toString()), + (prevData) => { + if (!prevData) return; + return { + ...prevData, + view_props: { + ...prevData?.view_props, + ...data, + }, + }; + }, + false + ); + + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { + ...oldData.view_props, + ...data, + }, + }); + }, + [myWorkspace, workspaceSlug] + ); + + const issueView = (myWorkspace?.view_props ?? initialValues).issueView; + const setIssueView = useCallback( + (newView: TIssueViewOptions) => { + console.log("newView", newView); + + saveData({ + issueView: newView, + }); + }, + [saveData] + ); + + const groupBy = (myWorkspace?.view_props ?? initialValues).groupByProperty; + const setGroupBy = useCallback( + (newGroup: TIssueGroupByOptions) => { + saveData({ + groupByProperty: newGroup, + }); + }, + [saveData] + ); + + const orderBy = (myWorkspace?.view_props ?? initialValues).orderBy; + const setOrderBy = useCallback( + (newOrderBy: TIssueOrderByOptions) => { + saveData({ + orderBy: newOrderBy, + }); + }, + [saveData] + ); + + const showEmptyGroups = (myWorkspace?.view_props ?? initialValues).showEmptyGroups; + const setShowEmptyGroups = useCallback(() => { + if (!myWorkspace) return; + + saveData({ + showEmptyGroups: !myWorkspace?.view_props?.showEmptyGroups, + }); + }, [myWorkspace, saveData]); + + const setProperty = useCallback( + (key: keyof Properties) => { + if (!myWorkspace) return; + + saveData({ + properties: { + ...myWorkspace.view_props?.properties, + [key]: !myWorkspace.view_props?.properties[key], + }, + }); + }, + [myWorkspace, saveData] + ); + + const filters = (myWorkspace?.view_props ?? initialValues).filters; + const setFilters = useCallback( + (updatedFilter: Partial) => { + if (!myWorkspace) return; + + saveData({ + filters: { + ...myWorkspace.view_props?.filters, + ...updatedFilter, + }, + }); + }, + [myWorkspace, saveData] + ); + + useEffect(() => { + if (!myWorkspace || !workspaceSlug) return; + + if (!myWorkspace.view_props) { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { ...initialValues }, + }); + } + }, [myWorkspace, workspaceSlug]); + + const newProperties: Properties = { + assignee: myWorkspace?.view_props.properties.assignee ?? true, + due_date: myWorkspace?.view_props.properties.due_date ?? true, + key: myWorkspace?.view_props.properties.key ?? true, + labels: myWorkspace?.view_props.properties.labels ?? true, + priority: myWorkspace?.view_props.properties.priority ?? true, + state: myWorkspace?.view_props.properties.state ?? true, + sub_issue_count: myWorkspace?.view_props.properties.sub_issue_count ?? true, + attachment_count: myWorkspace?.view_props.properties.attachment_count ?? true, + link: myWorkspace?.view_props.properties.link ?? true, + estimate: myWorkspace?.view_props.properties.estimate ?? true, + created_on: myWorkspace?.view_props.properties.created_on ?? true, + updated_on: myWorkspace?.view_props.properties.updated_on ?? true, + }; + + return { + issueView, + setIssueView, + groupBy, + setGroupBy, + orderBy, + setOrderBy, + showEmptyGroups, + setShowEmptyGroups, + properties: newProperties, + setProperty, + filters, + setFilters, + }; +}; + +export default useMyIssuesFilters; diff --git a/apps/app/hooks/my-issues/use-my-issues.tsx b/apps/app/hooks/my-issues/use-my-issues.tsx new file mode 100644 index 000000000..164fad4d4 --- /dev/null +++ b/apps/app/hooks/my-issues/use-my-issues.tsx @@ -0,0 +1,61 @@ +import { useMemo } from "react"; + +import useSWR from "swr"; + +// services +import userService from "services/user.service"; +// hooks +import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; +// types +import { IIssue } from "types"; +// fetch-keys +import { USER_ISSUES } from "constants/fetch-keys"; + +const useMyIssues = (workspaceSlug: string | undefined) => { + const { filters, groupBy, orderBy } = useMyIssuesFilters(workspaceSlug); + + const params: any = { + assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, + created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + group_by: groupBy, + labels: filters?.labels ? filters?.labels.join(",") : undefined, + order_by: orderBy, + priority: filters?.priority ? filters?.priority.join(",") : undefined, + state_group: filters?.state_group ? filters?.state_group.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, + type: filters?.type ? filters?.type : undefined, + }; + + const { data: myIssues, mutate: mutateMyIssues } = useSWR( + workspaceSlug ? USER_ISSUES(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => userService.userIssues(workspaceSlug.toString(), params) : null + ); + + const groupedIssues: + | { + [key: string]: IIssue[]; + } + | undefined = useMemo(() => { + if (!myIssues) return undefined; + + if (Array.isArray(myIssues)) + return { + allIssues: myIssues, + }; + + return myIssues; + }, [myIssues]); + + const isEmpty = + Object.values(groupedIssues ?? {}).every((group) => group.length === 0) || + Object.keys(groupedIssues ?? {}).length === 0; + + return { + groupedIssues, + isEmpty, + mutateMyIssues, + params, + }; +}; + +export default useMyIssues; diff --git a/apps/app/hooks/use-calendar-issues-view.tsx b/apps/app/hooks/use-calendar-issues-view.tsx index 41cf00cb7..d8daae922 100644 --- a/apps/app/hooks/use-calendar-issues-view.tsx +++ b/apps/app/hooks/use-calendar-issues-view.tsx @@ -41,12 +41,6 @@ const useCalendarIssuesView = () => { priority: filters?.priority ? filters?.priority.join(",") : undefined, type: filters?.type ? filters?.type : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, - issue__assignees__id: filters?.issue__assignees__id - ? filters?.issue__assignees__id.join(",") - : undefined, - issue__labels__id: filters?.issue__labels__id - ? filters?.issue__labels__id.join(",") - : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, target_date: calendarDateRange, }; diff --git a/apps/app/hooks/use-issues-view.tsx b/apps/app/hooks/use-issues-view.tsx index 1e5a2e555..85c756efc 100644 --- a/apps/app/hooks/use-issues-view.tsx +++ b/apps/app/hooks/use-issues-view.tsx @@ -57,12 +57,6 @@ const useIssuesView = () => { priority: filters?.priority ? filters?.priority.join(",") : undefined, type: filters?.type ? filters?.type : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, - issue__assignees__id: filters?.issue__assignees__id - ? filters?.issue__assignees__id.join(",") - : undefined, - issue__labels__id: filters?.issue__labels__id - ? filters?.issue__labels__id.join(",") - : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, sub_issue: showSubIssues, diff --git a/apps/app/hooks/use-issues.tsx b/apps/app/hooks/use-issues.tsx deleted file mode 100644 index 33e099676..000000000 --- a/apps/app/hooks/use-issues.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import useSWR from "swr"; -// services -import userService from "services/user.service"; -// types -import type { IIssue } from "types"; -// fetch-keys -import { USER_ISSUE } from "constants/fetch-keys"; - -const useIssues = (workspaceSlug: string | undefined) => { - // API Fetching - const { data: myIssues, mutate: mutateMyIssues } = useSWR( - workspaceSlug ? USER_ISSUE(workspaceSlug as string) : null, - workspaceSlug ? () => userService.userIssues(workspaceSlug as string) : null - ); - - return { - myIssues: myIssues, - mutateMyIssues, - }; -}; - -export default useIssues; diff --git a/apps/app/hooks/use-my-issues-filter.tsx b/apps/app/hooks/use-my-issues-filter.tsx deleted file mode 100644 index 46554aa43..000000000 --- a/apps/app/hooks/use-my-issues-filter.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import useSWR, { mutate } from "swr"; -// services -import workspaceService from "services/workspace.service"; -// hooks -import useUser from "hooks/use-user"; -// types -import { IWorkspaceMember, Properties } from "types"; -import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; - -const initialValues: Properties = { - assignee: true, - due_date: false, - key: true, - labels: false, - priority: false, - state: true, - sub_issue_count: false, - attachment_count: false, - link: false, - estimate: false, - created_on: false, - updated_on: false, -}; - -const useMyIssuesProperties = (workspaceSlug?: string) => { - const [properties, setProperties] = useState(initialValues); - - const { user } = useUser(); - - const { data: myWorkspace } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null, - workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug as string) : null, - { - shouldRetryOnError: false, - } - ); - - useEffect(() => { - if (!myWorkspace || !workspaceSlug || !user) return; - - setProperties({ ...initialValues, ...myWorkspace.view_props }); - - if (!myWorkspace.view_props) { - workspaceService.updateWorkspaceView(workspaceSlug, { - view_props: { ...initialValues }, - }); - } - }, [myWorkspace, workspaceSlug, user]); - - const updateIssueProperties = useCallback( - (key: keyof Properties) => { - if (!workspaceSlug || !user) return; - - setProperties((prev) => ({ ...prev, [key]: !prev[key] })); - - if (myWorkspace) { - mutate( - WORKSPACE_MEMBERS_ME(workspaceSlug.toString()), - (prevData) => { - if (!prevData) return; - return { - ...prevData, - view_props: { ...prevData?.view_props, [key]: !prevData.view_props?.[key] }, - }; - }, - false - ); - if (myWorkspace.view_props) { - workspaceService.updateWorkspaceView(workspaceSlug, { - view_props: { - ...myWorkspace.view_props, - [key]: !myWorkspace.view_props[key], - }, - }); - } else { - workspaceService.updateWorkspaceView(workspaceSlug, { - view_props: { ...initialValues }, - }); - } - } - }, - [workspaceSlug, myWorkspace, user] - ); - - const newProperties: Properties = { - assignee: properties.assignee, - due_date: properties.due_date, - key: properties.key, - labels: properties.labels, - priority: properties.priority, - state: properties.state, - sub_issue_count: properties.sub_issue_count, - attachment_count: properties.attachment_count, - link: properties.link, - estimate: properties.estimate, - created_on: properties.created_on, - updated_on: properties.updated_on, - }; - - return [newProperties, updateIssueProperties] as const; -}; - -export default useMyIssuesProperties; diff --git a/apps/app/hooks/use-project-members.tsx b/apps/app/hooks/use-project-members.tsx index 9764ade75..464ed1f75 100644 --- a/apps/app/hooks/use-project-members.tsx +++ b/apps/app/hooks/use-project-members.tsx @@ -6,11 +6,14 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys"; // hooks import useUser from "./use-user"; -const useProjectMembers = (workspaceSlug: string, projectId: string) => { +const useProjectMembers = (workspaceSlug: string | undefined, projectId: string | undefined) => { const { user } = useUser(); // fetching project members - const { data: members } = useSWR(PROJECT_MEMBERS(projectId), () => - projectService.projectMembers(workspaceSlug, projectId) + const { data: members } = useSWR( + workspaceSlug && projectId ? PROJECT_MEMBERS(projectId) : null, + workspaceSlug && projectId + ? () => projectService.projectMembers(workspaceSlug, projectId) + : null ); const hasJoined = members?.some((item: any) => item.member.id === (user as any)?.id); diff --git a/apps/app/hooks/use-spreadsheet-issues-view.tsx b/apps/app/hooks/use-spreadsheet-issues-view.tsx index 6e7b66bec..797ddf7d6 100644 --- a/apps/app/hooks/use-spreadsheet-issues-view.tsx +++ b/apps/app/hooks/use-spreadsheet-issues-view.tsx @@ -42,12 +42,6 @@ const useSpreadsheetIssuesView = () => { priority: filters?.priority ? filters?.priority.join(",") : undefined, type: filters?.type ? filters?.type : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, - issue__assignees__id: filters?.issue__assignees__id - ? filters?.issue__assignees__id.join(",") - : undefined, - issue__labels__id: filters?.issue__labels__id - ? filters?.issue__labels__id.join(",") - : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, sub_issue: "false", }; diff --git a/apps/app/hooks/use-workspace-members.tsx b/apps/app/hooks/use-workspace-members.tsx index 0a560b0f9..d4104d3bd 100644 --- a/apps/app/hooks/use-workspace-members.tsx +++ b/apps/app/hooks/use-workspace-members.tsx @@ -6,12 +6,14 @@ import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; // hooks import useUser from "./use-user"; -const useWorkspaceMembers = (workspaceSlug: string) => { +const useWorkspaceMembers = (workspaceSlug: string | undefined, fetchCondition?: boolean) => { + fetchCondition = fetchCondition ?? true; + const { user } = useUser(); const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug) : null, - workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug) : null + workspaceSlug && fetchCondition ? WORKSPACE_MEMBERS(workspaceSlug) : null, + workspaceSlug && fetchCondition ? () => workspaceService.workspaceMembers(workspaceSlug) : null ); const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id); diff --git a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx index 08e849d2e..85b9089c3 100644 --- a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx +++ b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx @@ -1,40 +1,67 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useRouter } from "next/router"; -// headless ui -import { Disclosure, Popover, Transition } from "@headlessui/react"; // icons -import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline"; -// images -import emptyMyIssues from "public/empty-state/my-issues.svg"; +import { PlusIcon } from "@heroicons/react/24/outline"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // hooks -import useIssues from "hooks/use-issues"; -// ui -import { Spinner, PrimaryButton, EmptyState } from "components/ui"; -import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; -// hooks -import useMyIssuesProperties from "hooks/use-my-issues-filter"; -// types -import { IIssue, Properties } from "types"; +import useProjects from "hooks/use-projects"; +import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; // components -import { MyIssuesListItem } from "components/issues"; -// helpers -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +import { MyIssuesView, MyIssuesViewOptions } from "components/issues"; +// ui +import { PrimaryButton } from "components/ui"; +import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; // types import type { NextPage } from "next"; -import useProjects from "hooks/use-projects"; +import useUser from "hooks/use-user"; const MyIssuesPage: NextPage = () => { const router = useRouter(); const { workspaceSlug } = router.query; - const { myIssues } = useIssues(workspaceSlug as string); const { projects } = useProjects(); + const { user } = useUser(); - const [properties, setProperties] = useMyIssuesProperties(workspaceSlug as string); + const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString()); + + const tabsList = [ + { + key: "assigned", + label: "Assigned to me", + selected: (filters?.assignees ?? []).length > 0, + onClick: () => { + setFilters({ + assignees: [user?.id ?? ""], + created_by: null, + }); + }, + }, + { + key: "created", + label: "Created by me", + selected: (filters?.created_by ?? []).length > 0, + onClick: () => { + setFilters({ + created_by: [user?.id ?? ""], + assignees: null, + }); + }, + }, + ]; + + useEffect(() => { + if (!filters || !user) return; + + if (!filters.assignees && !filters.created_by) { + setFilters({ + assignees: [user.id], + }); + return; + } + }, [filters, setFilters, user]); return ( { } right={
- {myIssues && myIssues.length > 0 && ( - - {({ open }) => ( - <> - - View - - - - -
-

Properties

-
- {Object.keys(properties).map((key) => { - if (key === "estimate" || key === "created_on" || key === "updated_on") - return null; - - return ( - - ); - })} -
-
-
-
- - )} -
- )} + { @@ -111,87 +86,26 @@ const MyIssuesPage: NextPage = () => {
} > -
- {myIssues ? ( - <> - {myIssues.length > 0 ? ( - - {({ open }) => ( -
-
- -
-

My Issues

- - {myIssues.length} - -
-
-
- - - {myIssues.map((issue: IIssue) => ( - - ))} - - -
- )} -
- ) : ( - 0 - ? "You don't have any issue assigned to you yet" - : "Issues assigned to you will appear here" - : "" - } - description={ - projects - ? projects.length > 0 - ? "Keep track of your work in a single place." - : "Let's create your first project and add issues that you want to accomplish." - : "" - } - image={emptyMyIssues} - buttonText={projects ? (projects.length > 0 ? "New Issue" : "New Project") : ""} - buttonIcon={} - onClick={() => { - let e: KeyboardEvent; - - if (projects && projects.length > 0) - e = new KeyboardEvent("keydown", { - key: "c", - }); - else - e = new KeyboardEvent("keydown", { - key: "p", - }); - - document.dispatchEvent(e); - }} - /> - )} - - ) : ( -
- +
+
+
+ {tabsList.map((tab) => ( + + ))}
- )} +
+
); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 690300358..73d6b4f80 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { IssueViewContextProvider } from "contexts/issue-view.context"; // components import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core"; -import { CycleDetailsSidebar } from "components/cycles"; +import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles"; // services import issuesService from "services/issues.service"; import cycleServices from "services/cycles.service"; @@ -36,6 +36,7 @@ const SingleCycle: React.FC = () => { const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleSidebar, setCycleSidebar] = useState(true); const [analyticsModal, setAnalyticsModal] = useState(false); + const [transferIssuesModal, setTransferIssuesModal] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -157,16 +158,22 @@ const SingleCycle: React.FC = () => {
} > + setTransferIssuesModal(false)} + isOpen={transferIssuesModal} + /> setAnalyticsModal(false)} />
+ {cycleStatus === "completed" && ( + setTransferIssuesModal(true)} /> + )}
{ analyticsModal ? "mr-[50%]" : "" } duration-300`} > - +
{ name="emoji_and_icon" render={({ field: { value, onChange } }) => ( - {value.name} - - ) : ( - renderEmoji(value) - ) - ) : ( - "Icon" - ) - } + label={value ? renderEmoji(value) : "Icon"} value={value} onChange={onChange} /> diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts index 1b32f04e7..ea92e6dec 100644 --- a/apps/app/services/issues.service.ts +++ b/apps/app/services/issues.service.ts @@ -248,6 +248,14 @@ class ProjectIssuesServices extends APIService { }); } + async getWorkspaceLabels(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/labels/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getIssueLabels(workspaceSlug: string, projectId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`) .then((response) => response?.data) diff --git a/apps/app/services/user.service.ts b/apps/app/services/user.service.ts index 955867fcf..d05ac7f63 100644 --- a/apps/app/services/user.service.ts +++ b/apps/app/services/user.service.ts @@ -4,6 +4,7 @@ import trackEventServices from "services/track-event.service"; import type { ICurrentUserResponse, + IIssue, IUser, IUserActivityResponse, IUserWorkspaceDashboard, @@ -26,8 +27,18 @@ class UserService extends APIService { }; } - async userIssues(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`) + async userIssues( + workspaceSlug: string, + params: any + ): Promise< + | { + [key: string]: IIssue[]; + } + | IIssue[] + > { + return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`, { + params, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/apps/app/services/workspace.service.ts b/apps/app/services/workspace.service.ts index e2598e1be..c9824c26a 100644 --- a/apps/app/services/workspace.service.ts +++ b/apps/app/services/workspace.service.ts @@ -14,6 +14,7 @@ import { IProductUpdateResponse, ICurrentUserResponse, IWorkspaceBulkInviteFormData, + IWorkspaceViewProps, } from "types"; const trackEvent = @@ -169,7 +170,10 @@ class WorkspaceService extends APIService { }); } - async updateWorkspaceView(workspaceSlug: string, data: any): Promise { + async updateWorkspaceView( + workspaceSlug: string, + data: { view_props: IWorkspaceViewProps } + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data) .then((response) => response?.data) .catch((error) => { diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 27f595774..22706e0e3 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -232,15 +232,20 @@ export interface IIssueFilterOptions { target_date: string[] | null; state: string[] | null; labels: string[] | null; - issue__assignees__id: string[] | null; - issue__labels__id: string[] | null; priority: string[] | null; created_by: string[] | null; } export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; -export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null; +export type TIssueGroupByOptions = + | "state" + | "priority" + | "labels" + | "created_by" + | "state_detail.group" + | "project" + | null; export type TIssueOrderByOptions = | "-created_at" @@ -279,3 +284,14 @@ export interface IIssueAttachment { updated_by: string; workspace: string; } + +export interface IIssueViewProps { + groupedIssues: { [key: string]: IIssue[] } | undefined; + groupByProperty: TIssueGroupByOptions; + isEmpty: boolean; + issueView: TIssueViewOptions; + orderBy: TIssueOrderByOptions; + params: any; + properties: Properties; + showEmptyGroups: boolean; +} diff --git a/apps/app/types/views.d.ts b/apps/app/types/views.d.ts index f599d2f6d..9bd133600 100644 --- a/apps/app/types/views.d.ts +++ b/apps/app/types/views.d.ts @@ -15,26 +15,11 @@ export interface IView { } export interface IQuery { - state: string[] | null; - parent: string[] | null; - priority: string[] | null; - labels: string[] | null; assignees: string[] | null; created_by: string[] | null; - name: string | null; - created_at: [ - { - datetime: string; - timeline: "before"; - }, - { - datetime: string; - timeline: "after"; - } - ]; - updated_at: string[] | null; - start_date: string[] | null; + labels: string[] | null; + priority: string[] | null; + state: string[] | null; target_date: string[] | null; - completed_at: string[] | null; - type: string; + type: "active" | "backlog" | null; } diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index cbc93c6b9..32215677e 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -1,4 +1,12 @@ -import type { IProjectMember, IUser, IUserLite } from "types"; +import type { + IIssueFilterOptions, + IProjectMember, + IUser, + IUserLite, + TIssueGroupByOptions, + TIssueOrderByOptions, + TIssueViewOptions, +} from "types"; export interface IWorkspace { readonly id: string; @@ -54,6 +62,19 @@ export type Properties = { updated_on: boolean; }; +export interface IWorkspaceViewProps { + properties: Properties; + issueView: TIssueViewOptions; + groupByProperty: TIssueGroupByOptions; + orderBy: TIssueOrderByOptions; + filters: Partial< + IIssueFilterOptions & { + state_group: string[] | null; + } + >; + showEmptyGroups: boolean; +} + export interface IWorkspaceMember { readonly id: string; user: IUserLite; @@ -61,7 +82,7 @@ export interface IWorkspaceMember { member: IUserLite; role: 5 | 10 | 15 | 20; company_role: string | null; - view_props: Properties; + view_props: IWorkspaceViewProps; created_at: Date; updated_at: Date; created_by: string;