From 928ebdf6322bb31a459c5f04dbdcb4741971d620 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 15 Mar 2023 11:44:44 +0530 Subject: [PATCH] fix: mutation for issue update on both kanban & list (#436) * refactor: issues filter logic * fix: removed fetch logic from hooks * feat: filter by assignee and label * chore: remove filter buttons * feat: filter options * fix: mutation for issue update on both kanban & list --------- Co-authored-by: Aaryan Khandelwal --- .../components/core/board-view/all-boards.tsx | 77 ++-- .../core/board-view/board-header.tsx | 50 +-- .../core/board-view/single-board.tsx | 45 +- .../core/board-view/single-issue.tsx | 117 +++--- .../components/core/issues-view-filter.tsx | 386 ++++++++---------- apps/app/components/core/issues-view.tsx | 349 +++++++++------- .../components/core/list-view/all-lists.tsx | 67 ++- .../core/list-view/single-issue.tsx | 98 ++--- .../components/core/list-view/single-list.tsx | 35 +- .../core/sidebar/sidebar-progress-stats.tsx | 11 +- .../core/sidebar/single-progress-stats.tsx | 11 +- apps/app/components/cycles/sidebar.tsx | 54 +-- .../components/cycles/single-cycle-card.tsx | 3 +- .../issues/view-select/due-date.tsx | 2 + .../issues/view-select/priority.tsx | 6 +- .../components/issues/view-select/state.tsx | 8 +- apps/app/components/modules/sidebar.tsx | 6 +- apps/app/components/states/single-state.tsx | 1 - apps/app/constants/fetch-keys.ts | 7 + apps/app/constants/issue.ts | 92 ++++- apps/app/contexts/issue-view.context.tsx | 192 +++++++-- apps/app/contexts/theme.context.tsx | 2 +- apps/app/helpers/common.helper.ts | 1 + apps/app/hooks/use-issue-view.tsx | 126 ------ apps/app/hooks/use-issues-view.tsx | 121 ++++++ .../projects/[projectId]/cycles/[cycleId].tsx | 91 +---- .../projects/[projectId]/issues/index.tsx | 41 +- .../[projectId]/modules/[moduleId].tsx | 20 +- apps/app/services/cycles.service.ts | 79 +++- apps/app/services/issues.service.ts | 16 +- apps/app/services/modules.service.ts | 44 +- apps/app/types/issues.d.ts | 16 +- apps/app/types/projects.d.ts | 11 +- 33 files changed, 1149 insertions(+), 1036 deletions(-) delete mode 100644 apps/app/hooks/use-issue-view.tsx create mode 100644 apps/app/hooks/use-issues-view.tsx diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index 55517a827..27aaace9b 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -1,16 +1,14 @@ // hooks -import useIssueView from "hooks/use-issue-view"; +import useProjectIssuesView from "hooks/use-issues-view"; // components import { SingleBoard } from "components/core/board-view/single-board"; // types -import { IIssue, IProjectMember, IState, UserAuth } from "types"; +import { IIssue, IState, UserAuth } from "types"; type Props = { type: "issue" | "cycle" | "module"; - issues: IIssue[]; states: IState[] | undefined; - members: IProjectMember[] | undefined; - addIssueToState: (groupTitle: string, stateId: string | null) => void; + addIssueToState: (groupTitle: string) => void; makeIssueCopy: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void; openIssuesListModal?: (() => void) | null; @@ -22,9 +20,7 @@ type Props = { export const AllBoards: React.FC = ({ type, - issues, states, - members, addIssueToState, makeIssueCopy, handleEditIssue, @@ -34,56 +30,35 @@ export const AllBoards: React.FC = ({ removeIssue, userAuth, }) => { - const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues); + const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useProjectIssuesView(); return ( <> {groupedByIssues ? ( -
-
- {Object.keys(groupedByIssues).map((singleGroup, index) => { - const currentState = - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup) - : null; +
+ {Object.keys(groupedByIssues).map((singleGroup, index) => { + const currentState = + selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; - const stateId = - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup)?.id ?? null - : null; - - const bgColor = - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup)?.color - : "#000000"; - - return ( - addIssueToState(singleGroup, stateId)} - handleDeleteIssue={handleDeleteIssue} - openIssuesListModal={openIssuesListModal ?? null} - orderBy={orderBy} - handleTrashBox={handleTrashBox} - removeIssue={removeIssue} - userAuth={userAuth} - /> - ); - })} -
+ return ( + addIssueToState(singleGroup)} + handleDeleteIssue={handleDeleteIssue} + openIssuesListModal={openIssuesListModal ?? null} + handleTrashBox={handleTrashBox} + removeIssue={removeIssue} + userAuth={userAuth} + /> + ); + })}
- ) : ( -
Loading...
- )} + ) : null} ); }; diff --git a/apps/app/components/core/board-view/board-header.tsx b/apps/app/components/core/board-view/board-header.tsx index 8237eb8f6..e06d0ce8b 100644 --- a/apps/app/components/core/board-view/board-header.tsx +++ b/apps/app/components/core/board-view/board-header.tsx @@ -1,52 +1,42 @@ import React from "react"; +// hooks +import useIssuesView from "hooks/use-issues-view"; // icons import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline"; +import { getStateGroupIcon } from "components/icons"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IProjectMember, IState, NestedKeyOf } from "types"; -import { getStateGroupIcon } from "components/icons"; +import { IState } from "types"; type Props = { - groupedByIssues: { - [key: string]: IIssue[]; - }; currentState?: IState | null; - selectedGroup: NestedKeyOf | null; groupTitle: string; - bgColor?: string; addIssueToState: () => void; - members: IProjectMember[] | undefined; isCollapsed: boolean; setIsCollapsed: React.Dispatch>; }; export const BoardHeader: React.FC = ({ - groupedByIssues, currentState, - selectedGroup, groupTitle, - bgColor, addIssueToState, isCollapsed, setIsCollapsed, - members, }) => { - const createdBy = - selectedGroup === "created_by" - ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..." - : null; + const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView(); - let assignees: any; - if (selectedGroup === "assignees") { - assignees = groupTitle && groupTitle !== "" ? groupTitle.split(",") : []; - assignees = - assignees.length > 0 - ? assignees - .map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name) - .join(", ") - : "No assignee"; - } + let bgColor = "#000000"; + if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000"; + + if (selectedGroup === "priority") + groupTitle === "high" + ? (bgColor = "#dc2626") + : groupTitle === "medium" + ? (bgColor = "#f97316") + : groupTitle === "low" + ? (bgColor = "#22c55e") + : (bgColor = "#ff0000"); return (
= ({ writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb", }} > - {selectedGroup === "created_by" - ? createdBy - : selectedGroup === "assignees" - ? assignees + {selectedGroup === "state" + ? addSpaceIfCamelCase(currentState?.name ?? "") : addSpaceIfCamelCase(groupTitle)} - {groupedByIssues[groupTitle].length} + {groupedByIssues?.[groupTitle].length ?? 0}
diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index 8e4226030..e987c700d 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/router"; 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"; @@ -16,24 +17,17 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types"; +import { IIssue, IState, UserAuth } from "types"; type Props = { type?: "issue" | "cycle" | "module"; currentState?: IState | null; - bgColor?: string; groupTitle: string; - groupedByIssues: { - [key: string]: IIssue[]; - }; - selectedGroup: NestedKeyOf | null; - members: IProjectMember[] | undefined; handleEditIssue: (issue: IIssue) => void; makeIssueCopy: (issue: IIssue) => void; addIssueToState: () => void; handleDeleteIssue: (issue: IIssue) => void; openIssuesListModal?: (() => void) | null; - orderBy: NestedKeyOf | null; handleTrashBox: (isDragging: boolean) => void; removeIssue: ((bridgeId: string) => void) | null; userAuth: UserAuth; @@ -42,17 +36,12 @@ type Props = { export const SingleBoard: React.FC = ({ type, currentState, - bgColor, groupTitle, - groupedByIssues, - selectedGroup, - members, handleEditIssue, makeIssueCopy, addIssueToState, handleDeleteIssue, openIssuesListModal, - orderBy, handleTrashBox, removeIssue, userAuth, @@ -60,35 +49,24 @@ export const SingleBoard: React.FC = ({ // collapse/expand const [isCollapsed, setIsCollapsed] = useState(true); + const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView(); + const router = useRouter(); const { workspaceSlug, projectId } = router.query; const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); - if (selectedGroup === "priority") - groupTitle === "high" - ? (bgColor = "#dc2626") - : groupTitle === "medium" - ? (bgColor = "#f97316") - : groupTitle === "low" - ? (bgColor = "#22c55e") - : (bgColor = "#ff0000"); - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( -
+
{(provided, snapshot) => ( @@ -115,14 +93,12 @@ export const SingleBoard: React.FC = ({
)} - {groupedByIssues[groupTitle].map((issue, index: number) => ( + {groupedByIssues?.[groupTitle].map((issue, index) => ( {(provided, snapshot) => ( = ({ provided={provided} snapshot={snapshot} type={type} - issue={issue} + index={index} selectedGroup={selectedGroup} + issue={issue} + groupTitle={groupTitle} properties={properties} editIssue={() => handleEditIssue(issue)} makeIssueCopy={() => makeIssueCopy(issue)} handleDeleteIssue={handleDeleteIssue} - orderBy={orderBy} handleTrashBox={handleTrashBox} removeIssue={() => { - removeIssue && removeIssue(issue.bridge); + if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id); }} userAuth={userAuth} /> diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index cfb3c4ad7..60e02648e 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -15,6 +15,7 @@ import { // services import issuesService from "services/issues.service"; // hooks +import useIssuesView from "hooks/use-issues-view"; import useToast from "hooks/use-toast"; // components import { @@ -33,31 +34,30 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; // helpers +import { handleIssuesMutation } from "constants/issue"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { - CycleIssueResponse, - IIssue, - ModuleIssueResponse, - NestedKeyOf, - Properties, - UserAuth, -} from "types"; +import { IIssue, Properties, UserAuth } from "types"; // fetch-keys -import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { + CYCLE_ISSUES_WITH_PARAMS, + MODULE_ISSUES_WITH_PARAMS, + PROJECT_ISSUES_LIST_WITH_PARAMS, +} from "constants/fetch-keys"; type Props = { type?: string; provided: DraggableProvided; snapshot: DraggableStateSnapshot; issue: IIssue; - selectedGroup: NestedKeyOf | null; properties: Properties; + groupTitle?: string; + index: number; + selectedGroup: "priority" | "state" | "labels" | null; editIssue: () => void; makeIssueCopy: () => void; removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; - orderBy: NestedKeyOf | null; handleTrashBox: (isDragging: boolean) => void; userAuth: UserAuth; }; @@ -67,13 +67,14 @@ export const SingleBoardIssue: React.FC = ({ provided, snapshot, issue, - selectedGroup, properties, + index, + selectedGroup, editIssue, makeIssueCopy, removeIssue, + groupTitle, handleDeleteIssue, - orderBy, handleTrashBox, userAuth, }) => { @@ -81,6 +82,8 @@ export const SingleBoardIssue: React.FC = ({ const [contextMenu, setContextMenu] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const { orderBy } = useIssuesView(); + const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -91,75 +94,55 @@ export const SingleBoardIssue: React.FC = ({ if (!workspaceSlug || !projectId) return; if (cycleId) - mutate( - CYCLE_ISSUES(cycleId as string), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - assignees: formData.assignees_list ?? p.issue_detail.assignees_list, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + CYCLE_ISSUES_WITH_PARAMS(cycleId as string), + (prevData) => + handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData), false ); if (moduleId) - mutate( - MODULE_ISSUES(moduleId as string), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - assignees: formData.assignees_list ?? p.issue_detail.assignees_list, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + MODULE_ISSUES_WITH_PARAMS(moduleId as string), + (prevData) => + handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData), false ); - mutate( - PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string), (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issue.id) - return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list }; - - return p; - }), - + handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData), false ); issuesService .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) .then((res) => { - if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); - if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); - - mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string)); + if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string)); + mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string)); }) .catch((error) => { console.log(error); }); }, - [workspaceSlug, projectId, cycleId, moduleId, issue] + [workspaceSlug, projectId, cycleId, moduleId, issue, groupTitle, index, selectedGroup] ); const getStyle = ( @@ -168,9 +151,7 @@ export const SingleBoardIssue: React.FC = ({ ) => { if (orderBy === "sort_order") return style; if (!snapshot.isDragging) return {}; - if (!snapshot.isDropAnimating) { - return style; - } + if (!snapshot.isDropAnimating) return style; return { ...style, @@ -301,7 +282,7 @@ export const SingleBoardIssue: React.FC = ({ {properties.labels && issue.label_details.length > 0 && (
{issue.label_details.map((label) => ( - @@ -312,7 +293,7 @@ export const SingleBoardIssue: React.FC = ({ }} /> {label.name} - +
))}
)} diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/issues-view-filter.tsx index f6e8bd662..bff7113d3 100644 --- a/apps/app/components/core/issues-view-filter.tsx +++ b/apps/app/components/core/issues-view-filter.tsx @@ -10,7 +10,7 @@ import issuesService from "services/issues.service"; import stateService from "services/state.service"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; -import useIssueView from "hooks/use-issue-view"; +import useIssuesView from "hooks/use-issues-view"; // headless ui import { Popover, Transition } from "@headlessui/react"; // ui @@ -29,11 +29,7 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fet import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; import { PRIORITIES } from "constants/project"; -type Props = { - issues?: IIssue[]; -}; - -export const IssuesFilterView: React.FC = ({ issues }) => { +export const IssuesFilterView: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -44,12 +40,12 @@ export const IssuesFilterView: React.FC = ({ issues }) => { groupByProperty, setGroupByProperty, setOrderBy, - setFilterIssue, orderBy, - filterIssue, + filters, + setFilters, resetFilterToDefault, setNewFilterDefaultView, - } = useIssueView(issues ?? []); + } = useIssuesView(); const [properties, setProperties] = useIssuesProperties( workspaceSlug as string, @@ -79,208 +75,182 @@ export const IssuesFilterView: React.FC = ({ issues }) => { ); return ( - <> - {issues && issues.length > 0 && ( -
-
- - -
- - Filters - - } +
+
+ + +
+ -

Status

- {statesList?.map((state) => ( - {}}> - <>{state.name} - - ))} -

Members

- {members?.map((member) => ( - {}}> - <> - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name + " " + member.member.last_name - : member.member.email} - - - ))} -

Labels

- {issueLabels?.map((label) => ( - {}}> - <>{label.name} - - ))} -

Priority

- {PRIORITIES?.map((priority) => ( - {}}> - {priority ?? "None"} - - ))} -
- - {({ open }) => ( - <> - - View - + Filters +
); }; diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 779338321..a6357117c 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -12,13 +12,13 @@ import stateService from "services/state.service"; import projectService from "services/project.service"; import modulesService from "services/modules.service"; // hooks -import useIssueView from "hooks/use-issue-view"; +import useIssuesView from "hooks/use-issues-view"; // components import { AllLists, AllBoards } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; // icons -import { TrashIcon } from "@heroicons/react/24/outline"; +import { PlusIcon, RectangleStackIcon, TrashIcon } from "@heroicons/react/24/outline"; // helpers import { getStatesList } from "helpers/state.helper"; // types @@ -26,32 +26,29 @@ import { CycleIssueResponse, IIssue, ModuleIssueResponse, UserAuth } from "types // fetch-keys import { CYCLE_ISSUES, + CYCLE_ISSUES_WITH_PARAMS, MODULE_ISSUES, - PROJECT_ISSUES_LIST, + MODULE_ISSUES_WITH_PARAMS, + PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_MEMBERS, STATE_LIST, } from "constants/fetch-keys"; +import { EmptySpace, EmptySpaceItem } from "components/ui"; type Props = { type?: "issue" | "cycle" | "module"; - issues: IIssue[]; openIssuesListModal?: () => void; userAuth: UserAuth; }; -export const IssuesView: React.FC = ({ - type = "issue", - issues, - openIssuesListModal, - userAuth, -}) => { +export const IssuesView: React.FC = ({ type = "issue", openIssuesListModal, userAuth }) => { // create issue modal const [createIssueModal, setCreateIssueModal] = useState(false); const [preloadedData, setPreloadedData] = useState< (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined >(undefined); - // updates issue modal + // update issue modal const [editIssueModal, setEditIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState< (IIssue & { actionType: "edit" | "delete" }) | undefined @@ -68,11 +65,13 @@ export const IssuesView: React.FC = ({ const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { - issueView, groupedByIssues, + issueView, groupByProperty: selectedGroup, orderBy, - } = useIssueView(issues); + filters, + setFilters, + } = useIssuesView(); const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, @@ -101,7 +100,7 @@ export const IssuesView: React.FC = ({ (result: DropResult) => { setTrashBox(false); - if (!result.destination || !workspaceSlug || !projectId) return; + if (!result.destination || !workspaceSlug || !projectId || !groupedByIssues) return; const { source, destination } = result; @@ -156,90 +155,99 @@ export const IssuesView: React.FC = ({ draggedItem.sort_order = newSortOrder; } - if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) { - const sourceGroup = source.droppableId; // source group id - const destinationGroup = destination.droppableId; // destination group id + const destinationGroup = destination.droppableId; // destination group id - if (!sourceGroup || !destinationGroup) return; + if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) { + // different group/column; + + // source.droppableId !== destination.droppableId -> even if order by is not sort_order, + // if the issue is moved to a different group, then we will change the group of the + // dragged item(or issue) if (selectedGroup === "priority") draggedItem.priority = destinationGroup; - else if (selectedGroup === "state_detail.name") { - const destinationState = states?.find((s) => s.name === destinationGroup); + else if (selectedGroup === "state") draggedItem.state = destinationGroup; + } - if (!destinationState) return; + const sourceGroup = source.droppableId; // source group id - draggedItem.state = destinationState.id; - draggedItem.state_detail = destinationState; - } - - if (cycleId) - mutate( - CYCLE_ISSUES(cycleId as string), - (prevData) => { - if (!prevData) return prevData; - const updatedIssues = prevData.map((issue) => { - if (issue.issue_detail.id === draggedItem.id) { - return { - ...issue, - issue_detail: draggedItem, - }; - } - return issue; - }); - return [...updatedIssues]; - }, - false - ); - - if (moduleId) - mutate( - MODULE_ISSUES(moduleId as string), - (prevData) => { - if (!prevData) return prevData; - const updatedIssues = prevData.map((issue) => { - if (issue.issue_detail.id === draggedItem.id) { - return { - ...issue, - issue_detail: draggedItem, - }; - } - return issue; - }); - return [...updatedIssues]; - }, - false - ); - - mutate( - PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), + // TODO: move this mutation logic to a separate function + if (cycleId) + mutate<{ + [key: string]: IIssue[]; + }>( + CYCLE_ISSUES_WITH_PARAMS(cycleId as string), (prevData) => { if (!prevData) return prevData; - const updatedIssues = prevData.map((i) => { - if (i.id === draggedItem.id) return draggedItem; + const sourceGroupArray = prevData[sourceGroup]; + const destinationGroupArray = prevData[destinationGroup]; - return i; - }); + sourceGroupArray.splice(source.index, 1); + destinationGroupArray.splice(destination.index, 0, draggedItem); - return updatedIssues; + return { + ...prevData, + [sourceGroup]: sourceGroupArray, + [destinationGroup]: destinationGroupArray, + }; + }, + false + ); + else if (moduleId) + mutate<{ + [key: string]: IIssue[]; + }>( + MODULE_ISSUES_WITH_PARAMS(moduleId as string), + (prevData) => { + if (!prevData) return prevData; + + const sourceGroupArray = prevData[sourceGroup]; + const destinationGroupArray = prevData[destinationGroup]; + + sourceGroupArray.splice(source.index, 1); + destinationGroupArray.splice(destination.index, 0, draggedItem); + + return { + ...prevData, + [sourceGroup]: sourceGroupArray, + [destinationGroup]: destinationGroupArray, + }; + }, + false + ); + else + mutate<{ [key: string]: IIssue[] }>( + PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string), + (prevData) => { + if (!prevData) return prevData; + + const sourceGroupArray = prevData[sourceGroup]; + const destinationGroupArray = prevData[destinationGroup]; + + sourceGroupArray.splice(source.index, 1); + destinationGroupArray.splice(destination.index, 0, draggedItem); + + return { + ...prevData, + [sourceGroup]: sourceGroupArray, + [destinationGroup]: destinationGroupArray, + }; }, false ); - // patch request - issuesService - .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { - priority: draggedItem.priority, - state: draggedItem.state, - sort_order: draggedItem.sort_order, - }) - .then((res) => { - if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); - if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); - - mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); - }); - } + // patch request + issuesService + .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { + priority: draggedItem.priority, + state: draggedItem.state, + sort_order: draggedItem.sort_order, + }) + .then(() => { + if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); + if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); + mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string)); + }); } }, [ @@ -250,17 +258,15 @@ export const IssuesView: React.FC = ({ projectId, selectedGroup, orderBy, - states, handleDeleteIssue, ] ); const addIssueToState = useCallback( - (groupTitle: string, stateId: string | null) => { + (groupTitle: string) => { setCreateIssueModal(true); if (selectedGroup) setPreloadedData({ - state: stateId ?? undefined, [selectedGroup]: groupTitle, actionType: "createIssue", }); @@ -372,69 +378,116 @@ export const IssuesView: React.FC = ({ isOpen={deleteIssueModal} data={issueToDelete} /> - -
- - - {(provided, snapshot) => ( -
+ {Object.keys(filters).map((key) => { + if (filters[key as keyof typeof filters] !== null) + return ( +
- )} -
- {issueView === "list" ? ( - - ) : ( - - )} -
+ Remove {key} filter + + ); + })}
+ + + {(provided, snapshot) => ( +
+ + Drop issue here to delete + {provided.placeholder} +
+ )} +
+ {groupedByIssues ? ( + Object.keys(groupedByIssues).length > 0 ? ( + <> + {issueView === "list" ? ( + + ) : ( + + )} + + ) : ( +
+ + + Use
C
shortcut to + create a new issue + + } + Icon={PlusIcon} + action={() => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }} + /> +
+
+ ) + ) : ( +

Loading...

+ )} +
); }; diff --git a/apps/app/components/core/list-view/all-lists.tsx b/apps/app/components/core/list-view/all-lists.tsx index 0e067513d..750dd3bc2 100644 --- a/apps/app/components/core/list-view/all-lists.tsx +++ b/apps/app/components/core/list-view/all-lists.tsx @@ -1,5 +1,5 @@ // hooks -import useIssueView from "hooks/use-issue-view"; +import useIssuesView from "hooks/use-issues-view"; // components import { SingleList } from "components/core/list-view/single-list"; // types @@ -8,7 +8,6 @@ import { IIssue, IProjectMember, IState, UserAuth } from "types"; // types type Props = { type: "issue" | "cycle" | "module"; - issues: IIssue[]; states: IState[] | undefined; members: IProjectMember[] | undefined; addIssueToState: (groupTitle: string, stateId: string | null) => void; @@ -22,7 +21,6 @@ type Props = { export const AllLists: React.FC = ({ type, - issues, states, members, addIssueToState, @@ -33,44 +31,35 @@ export const AllLists: React.FC = ({ removeIssue, userAuth, }) => { - const { groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues); + const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView(); return ( -
- {Object.keys(groupedByIssues).map((singleGroup) => { - const currentState = - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup) - : null; - const stateId = - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup)?.id ?? null - : null; - const bgColor = - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup)?.color - : "#000000"; + <> + {groupedByIssues && ( +
+ {Object.keys(groupedByIssues).map((singleGroup) => { + const stateId = selectedGroup === "state" ? singleGroup : null; - return ( - addIssueToState(singleGroup, stateId)} - makeIssueCopy={makeIssueCopy} - handleEditIssue={handleEditIssue} - handleDeleteIssue={handleDeleteIssue} - openIssuesListModal={type !== "issue" ? openIssuesListModal : null} - removeIssue={removeIssue} - userAuth={userAuth} - /> - ); - })} -
+ return ( + addIssueToState(singleGroup, stateId)} + makeIssueCopy={makeIssueCopy} + handleEditIssue={handleEditIssue} + handleDeleteIssue={handleDeleteIssue} + openIssuesListModal={type !== "issue" ? openIssuesListModal : null} + removeIssue={removeIssue} + userAuth={userAuth} + /> + ); + })} +
+ )} + ); }; diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index bfbec1db7..e8900f432 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -16,7 +16,8 @@ import { ViewPrioritySelect, ViewStateSelect, } from "components/issues/view-select"; - +// hooks +import useIssueView from "hooks/use-issues-view"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons @@ -28,16 +29,23 @@ import { } from "@heroicons/react/24/outline"; // helpers import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +import { handleIssuesMutation } from "constants/issue"; // types -import { CycleIssueResponse, IIssue, ModuleIssueResponse, Properties, UserAuth } from "types"; +import { IIssue, Properties, UserAuth } from "types"; // fetch-keys -import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { + CYCLE_ISSUES_WITH_PARAMS, + MODULE_ISSUES_WITH_PARAMS, + PROJECT_ISSUES_LIST_WITH_PARAMS, +} 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; @@ -49,8 +57,10 @@ export const SingleListIssue: React.FC = ({ issue, properties, editIssue, + index, makeIssueCopy, removeIssue, + groupTitle, handleDeleteIssue, userAuth, }) => { @@ -63,80 +73,62 @@ export const SingleListIssue: React.FC = ({ const { setToastAlert } = useToast(); + const { groupByProperty: selectedGroup } = useIssueView(); + const partialUpdateIssue = useCallback( (formData: Partial) => { if (!workspaceSlug || !projectId) return; if (cycleId) - mutate( - CYCLE_ISSUES(cycleId as string), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - assignees: formData.assignees_list ?? p.issue_detail.assignees_list, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + CYCLE_ISSUES_WITH_PARAMS(cycleId as string), + (prevData) => + handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData), false ); if (moduleId) - mutate( - MODULE_ISSUES(moduleId as string), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - assignees: formData.assignees_list ?? p.issue_detail.assignees_list, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + MODULE_ISSUES_WITH_PARAMS(moduleId as string), + (prevData) => + handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData), false ); - mutate( - PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), + mutate< + | { + [key: string]: IIssue[]; + } + | IIssue[] + >( + PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string), (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issue.id) - return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list }; - - return p; - }), - + handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData), false ); issuesService .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) .then((res) => { - if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); - if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); - - mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string)); + if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string)); + mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string)); }) .catch((error) => { console.log(error); }); }, - [workspaceSlug, projectId, cycleId, moduleId, issue] + [workspaceSlug, projectId, cycleId, moduleId, issue, groupTitle, index, selectedGroup] ); const handleCopyText = () => { diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx index 1ca09ff99..4b07fb330 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/list-view/single-list.tsx @@ -12,7 +12,7 @@ import { getStateGroupIcon } from "components/icons"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types"; +import { IIssue, IProjectMember, IState, UserAuth } from "types"; import { CustomMenu } from "components/ui"; type Props = { @@ -23,7 +23,7 @@ type Props = { groupedByIssues: { [key: string]: IIssue[]; }; - selectedGroup: NestedKeyOf | null; + selectedGroup: "priority" | "state" | "labels" | null; members: IProjectMember[] | undefined; addIssueToState: () => void; makeIssueCopy: (issue: IIssue) => void; @@ -55,22 +55,6 @@ export const SingleList: React.FC = ({ const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); - const createdBy = - selectedGroup === "created_by" - ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "Loading..." - : null; - - let assignees: any; - if (selectedGroup === "assignees") { - assignees = groupTitle && groupTitle !== "" ? groupTitle.split(",") : []; - assignees = - assignees.length > 0 - ? assignees - .map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name) - .join(", ") - : "No assignee"; - } - return ( {({ open }) => ( @@ -82,7 +66,7 @@ export const SingleList: React.FC = ({ >
- {selectedGroup !== null && selectedGroup === "state_detail.name" ? ( + {selectedGroup !== null && selectedGroup === "state" ? ( {currentState && getStateGroupIcon(currentState.group, "20", "20", bgColor)} @@ -91,11 +75,7 @@ export const SingleList: React.FC = ({ )} {selectedGroup !== null ? (

- {selectedGroup === "created_by" - ? createdBy - : selectedGroup === "assignees" - ? assignees - : addSpaceIfCamelCase(groupTitle)} + {addSpaceIfCamelCase(groupTitle)}

) : (

All Issues

@@ -105,7 +85,6 @@ export const SingleList: React.FC = ({
- {type === "issue" ? (
} completed={completeArray.length} diff --git a/apps/app/components/core/sidebar/single-progress-stats.tsx b/apps/app/components/core/sidebar/single-progress-stats.tsx index 58e684f61..6066a3deb 100644 --- a/apps/app/components/core/sidebar/single-progress-stats.tsx +++ b/apps/app/components/core/sidebar/single-progress-stats.tsx @@ -6,14 +6,23 @@ type TSingleProgressStatsProps = { title: any; completed: number; total: number; + onClick?: () => void; + selected?: boolean; }; export const SingleProgressStats: React.FC = ({ title, completed, total, + onClick, + selected = false, }) => ( -
+
{title}
diff --git a/apps/app/components/cycles/sidebar.tsx b/apps/app/components/cycles/sidebar.tsx index 674ac16c7..d45e72358 100644 --- a/apps/app/components/cycles/sidebar.tsx +++ b/apps/app/components/cycles/sidebar.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import Image from "next/image"; -import { mutate } from "swr"; +import useSWR, { mutate } from "swr"; // react-hook-form import { useForm } from "react-hook-form"; @@ -36,25 +36,17 @@ import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helpe import { groupBy } from "helpers/array.helper"; import { renderDateFormat, renderShortDate } from "helpers/date-time.helper"; // types -import { CycleIssueResponse, ICycle, IIssue } from "types"; +import { ICycle, IIssue } from "types"; // fetch-keys -import { CYCLE_DETAILS } from "constants/fetch-keys"; +import { CYCLE_DETAILS, CYCLE_ISSUES } from "constants/fetch-keys"; type Props = { - issues: IIssue[]; cycle: ICycle | undefined; isOpen: boolean; - cycleIssues: CycleIssueResponse[]; cycleStatus: string; }; -export const CycleDetailsSidebar: React.FC = ({ - issues, - cycle, - isOpen, - cycleIssues, - cycleStatus, -}) => { +export const CycleDetailsSidebar: React.FC = ({ cycle, isOpen, cycleStatus }) => { const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const [startDateRange, setStartDateRange] = useState(new Date()); const [endDateRange, setEndDateRange] = useState(null); @@ -69,13 +61,25 @@ export const CycleDetailsSidebar: React.FC = ({ end_date: new Date().toString(), }; + const { data: issues } = useSWR( + workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null, + workspaceSlug && projectId && cycleId + ? () => + cyclesService.getCycleIssues( + workspaceSlug as string, + projectId as string, + cycleId as string + ) + : null + ); + const groupedIssues = { backlog: [], unstarted: [], started: [], cancelled: [], completed: [], - ...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"), + ...groupBy(issues ?? [], "state_detail.group"), }; const { reset } = useForm({ @@ -131,9 +135,10 @@ export const CycleDetailsSidebar: React.FC = ({ const isStartValid = new Date(`${cycle?.start_date}`) <= new Date(); const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`); - const progressPercentage = cycleIssues - ? Math.round((groupedIssues.completed.length / cycleIssues?.length) * 100) + const progressPercentage = issues + ? Math.round((groupedIssues.completed.length / issues?.length) * 100) : null; + return ( <> @@ -305,10 +310,10 @@ export const CycleDetailsSidebar: React.FC = ({ - {groupedIssues.completed.length}/{cycleIssues?.length} + {groupedIssues.completed.length}/{issues?.length}
@@ -324,7 +329,7 @@ export const CycleDetailsSidebar: React.FC = ({
Progress - {!open && cycleIssues && progressPercentage ? ( + {!open && issues && progressPercentage ? ( {progressPercentage ? `${progressPercentage}%` : ""} @@ -359,7 +364,7 @@ export const CycleDetailsSidebar: React.FC = ({ Pending Issues -{" "} - {cycleIssues?.length - groupedIssues.completed.length}{" "} + {issues?.length ?? 0 - groupedIssues.completed.length}{" "}
@@ -376,7 +381,7 @@ export const CycleDetailsSidebar: React.FC = ({
@@ -403,7 +408,7 @@ export const CycleDetailsSidebar: React.FC = ({ Other Information
- {issues.length > 0 ? ( + {(issues?.length ?? 0) > 0 ? ( = ({
- {issues.length > 0 ? ( + {(issues?.length ?? 0) > 0 ? (
- +
) : ( "" diff --git a/apps/app/components/cycles/single-cycle-card.tsx b/apps/app/components/cycles/single-cycle-card.tsx index 23279d559..607c0cc04 100644 --- a/apps/app/components/cycles/single-cycle-card.tsx +++ b/apps/app/components/cycles/single-cycle-card.tsx @@ -30,7 +30,6 @@ import { capitalizeFirstLetter, copyTextToClipboard, truncateText } from "helper import { CompletedCyclesResponse, CurrentAndUpcomingCyclesResponse, - CycleIssueResponse, DraftCyclesResponse, ICycle, } from "types"; @@ -65,7 +64,7 @@ export const SingleCycleCard: React.FC = (props) => { const { workspaceSlug, projectId } = router.query; const { setToastAlert } = useToast(); - const { data: cycleIssues } = useSWR( + const { data: cycleIssues } = useSWR( workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null, workspaceSlug && projectId && cycle.id ? () => cyclesService.getCycleIssues(workspaceSlug as string, projectId as string, cycle.id) diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index b36ca6db0..f897eaad9 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -28,6 +28,8 @@ export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, onChange={(val) => partialUpdateIssue({ target_date: val, + priority: issue.priority, + state: issue.state, }) } className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"} diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 6ca81037e..e5078e71d 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -25,8 +25,10 @@ export const ViewPrioritySelect: React.FC = ({ isNotAllowed, }) => ( partialUpdateIssue({ priority: data })} + value={issue.priority} + onChange={(data: string) => + partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date }) + } maxHeight="md" customButton={
} > - {cycleIssuesArray ? ( - cycleIssuesArray.length > 0 ? ( -
- -
- ) : ( -
- - { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }} - /> - - -
- ) - ) : ( -
- -
- )} - +
+ +
+ ); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index 74ec6041c..952f1d488 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -5,7 +5,6 @@ import useSWR from "swr"; // lib import { requiredAdmin, requiredAuth } from "lib/auth"; // services -import issuesServices from "services/issues.service"; import projectService from "services/project.service"; // layouts import AppLayout from "layouts/app-layout"; @@ -14,31 +13,20 @@ import { IssueViewContextProvider } from "contexts/issue-view.context"; // components import { IssuesFilterView, IssuesView } from "components/core"; // ui -import { Spinner, EmptySpace, EmptySpaceItem, HeaderButton, EmptyState } from "components/ui"; +import { HeaderButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { RectangleStackIcon, PlusIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "@heroicons/react/24/outline"; // types import type { UserAuth } from "types"; import type { GetServerSidePropsContext, NextPage } from "next"; // fetch-keys -import { PROJECT_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; -// image -import emptyIssue from "public/empty-state/empty-issue.svg"; +import { PROJECT_DETAILS } from "constants/fetch-keys"; const ProjectIssues: NextPage = (props) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: projectIssues } = useSWR( - workspaceSlug && projectId - ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) - : null, - workspaceSlug && projectId - ? () => issuesServices.getIssues(workspaceSlug as string, projectId as string) - : null - ); - const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -57,7 +45,7 @@ const ProjectIssues: NextPage = (props) => { } right={
- p.parent === null) ?? []} /> + = (props) => {
} > - {projectIssues ? ( - projectIssues.length > 0 ? ( - p.parent === null) ?? []} - userAuth={props} - /> - ) : ( - - ) - ) : ( -
- -
- )} + ); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 7bfe0052e..7b0d0ae89 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -30,7 +30,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // helpers import { truncateText } from "helpers/string.helper"; // types -import { IModule, ModuleIssueResponse, UserAuth } from "types"; +import { IModule, UserAuth } from "types"; // fetch-keys import { @@ -63,7 +63,7 @@ const SingleModule: React.FC = (props) => { : null ); - const { data: moduleIssues } = useSWR( + const { data: moduleIssues } = useSWR( workspaceSlug && projectId && moduleId ? MODULE_ISSUES(moduleId as string) : null, workspaceSlug && projectId && moduleId ? () => @@ -87,13 +87,6 @@ const SingleModule: React.FC = (props) => { : null ); - const moduleIssuesArray = moduleIssues?.map((issue) => ({ - ...issue.issue_detail, - sub_issues_count: issue.sub_issues_count, - bridge: issue.id, - module: moduleId as string, - })); - const handleAddIssuesToModule = async (data: { issues: string[] }) => { if (!workspaceSlug || !projectId) return; @@ -153,7 +146,7 @@ const SingleModule: React.FC = (props) => {
- +
} > - {moduleIssuesArray ? ( - moduleIssuesArray.length > 0 ? ( + {moduleIssues ? ( + moduleIssues.length > 0 ? (
@@ -213,7 +205,7 @@ const SingleModule: React.FC = (props) => {
)} { + async getCycleIssues( + workspaceSlug: string, + projectId: string, + cycleId: string + ): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/` ) @@ -48,6 +60,22 @@ class ProjectCycleServices extends APIService { }); } + async getCycleIssuesWithParams( + workspaceSlug: string, + projectId: string, + cycleId: string, + queries?: IIssueViewOptions + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`, + { params: queries } + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async updateCycle( workspaceSlug: string, projectId: string, @@ -88,18 +116,28 @@ class ProjectCycleServices extends APIService { }); } - async cycleDateCheck(workspaceSlug: string, projectId: string, data: { - start_date: string, - end_date: string - }): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data) + async cycleDateCheck( + workspaceSlug: string, + projectId: string, + data: { + start_date: string; + end_date: string; + } + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise { + async getCurrentAndUpcomingCycles( + workspaceSlug: string, + projectId: string + ): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/` ) @@ -110,16 +148,17 @@ class ProjectCycleServices extends APIService { } async getDraftCycles(workspaceSlug: string, projectId: string): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/` - ) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async getCompletedCycles(workspaceSlug: string, projectId: string): Promise { + async getCompletedCycles( + workspaceSlug: string, + projectId: string + ): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/` ) @@ -136,21 +175,29 @@ class ProjectCycleServices extends APIService { cycle: string; } ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/`, data) + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async removeCycleFromFavorites(workspaceSlug: string, projectId: string, cycleId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/`) + async removeCycleFromFavorites( + workspaceSlug: string, + projectId: string, + cycleId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/` + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - } export default new ProjectCycleServices(); diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts index 036cf36db..cb362dc05 100644 --- a/apps/app/services/issues.service.ts +++ b/apps/app/services/issues.service.ts @@ -1,7 +1,7 @@ // services import APIService from "services/api.service"; // type -import type { IIssue, IIssueActivity, IIssueComment } from "types"; +import type { IIssue, IIssueActivity, IIssueComment, IIssueViewOptions } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -26,6 +26,20 @@ class ProjectIssuesServices extends APIService { }); } + async getIssuesWithParams( + workspaceSlug: string, + projectId: string, + queries?: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`) .then((response) => response?.data) diff --git a/apps/app/services/modules.service.ts b/apps/app/services/modules.service.ts index b874a7dc8..e1cde9cc9 100644 --- a/apps/app/services/modules.service.ts +++ b/apps/app/services/modules.service.ts @@ -1,7 +1,7 @@ // services import APIService from "services/api.service"; // types -import type { IModule } from "types"; +import type { IIssueViewOptions, IModule, ModuleIssueResponse, IIssue } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -76,7 +76,11 @@ class ProjectIssuesServices extends APIService { }); } - async getModuleIssues(workspaceSlug: string, projectId: string, moduleId: string): Promise { + async getModuleIssues( + workspaceSlug: string, + projectId: string, + moduleId: string + ): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/` ) @@ -86,6 +90,27 @@ class ProjectIssuesServices extends APIService { }); } + async getModuleIssuesWithParams( + workspaceSlug: string, + projectId: string, + moduleId: string, + queries?: IIssueViewOptions + ): Promise< + | IIssue[] + | { + [key: string]: IIssue[]; + } + > { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, + { params: queries } + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async addIssuesToModule( workspaceSlug: string, projectId: string, @@ -159,15 +184,24 @@ class ProjectIssuesServices extends APIService { module: string; } ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/`, data) + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async removeModuleFromFavorites(workspaceSlug: string, projectId: string, moduleId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/`) + async removeModuleFromFavorites( + workspaceSlug: string, + projectId: string, + moduleId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/` + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 091f47200..42d3cc84f 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -67,7 +67,7 @@ export interface IIssue { blockers: any[]; blockers_list: string[]; blocks_list: string[]; - bridge: string; + bridge_id?: string | null; completed_at: Date; created_at: Date; created_by: string; @@ -206,3 +206,17 @@ export interface IIssueActivity { issue_comment: string | null; actor: string; } + +export interface IIssueFilterOptions { + type: "active" | "backlog" | null; + assignees: string[] | null; + labels: string[] | null; + issue__assignees__id: string[] | null; + issue__labels__id: string[] | null; +} + +export interface IIssueViewOptions { + group_by: "state" | "priority" | "labels" | null; + order_by: "created_at" | "updated_at" | "priority" | "sort_order"; + filters: IIssueFilterOptions; +} diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts index a25f26a26..917f828d1 100644 --- a/apps/app/types/projects.d.ts +++ b/apps/app/types/projects.d.ts @@ -1,4 +1,4 @@ -import type { IUserLite, IWorkspace } from "./"; +import type { IIssueFilterOptions, IUserLite, IWorkspace } from "./"; export interface IProject { cover_image: string | null; @@ -34,11 +34,10 @@ export interface IFavoriteProject { } type ProjectViewTheme = { - collapsed: boolean; - issueView: "list" | "kanban" | null; - groupByProperty: NestedKeyOf | null; - filterIssue: "activeIssue" | "backlogIssue" | null; - orderBy: NestedKeyOf | null; + issueView: "list" | "kanban"; + groupByProperty: "state" | "priority" | "labels" | null; + orderBy: "created_at" | "updated_at" | "priority" | "sort_order"; + filters: IIssueFilterOptions; }; export interface IProjectMember {