diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index e3e187d60..c50ce7251 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -2,6 +2,7 @@ export * from "./board-view"; export * from "./calendar-view"; export * from "./gantt-chart-view"; export * from "./list-view"; +export * from "./spreadsheet-view"; export * from "./sidebar"; export * from "./bulk-delete-issues-modal"; export * from "./existing-issues-list-modal"; diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/issues-view-filter.tsx index 6856e8f8b..679f6adc3 100644 --- a/apps/app/components/core/issues-view-filter.tsx +++ b/apps/app/components/core/issues-view-filter.tsx @@ -10,7 +10,7 @@ import { Popover, Transition } from "@headlessui/react"; // components import { SelectFilters } from "components/views"; // ui -import { CustomMenu, ToggleSwitch } from "components/ui"; +import { CustomMenu, Icon, ToggleSwitch } from "components/ui"; // icons import { ChevronDownIcon, @@ -83,6 +83,15 @@ export const IssuesFilterView: React.FC = () => { > + + )} + + + + + {issue.name} + + + + {properties.state && ( +
+ +
+ )} + {properties.priority && ( +
+ +
+ )} + {properties.assignee && ( +
+ +
+ )} + {properties.labels ? ( + issue.label_details.length > 0 ? ( +
+ {issue.label_details.slice(0, 4).map((label, index) => ( +
+ +
+ ))} + {issue.label_details.length > 4 ? +{issue.label_details.length - 4} : null} +
+ ) : ( +
+ No Labels +
+ ) + ) : ( + "" + )} + {properties.due_date && ( +
+ +
+ )} + {properties.estimate && ( +
+ +
+ )} +
+ {!isNotAllowed && ( + + +
+ + Edit issue +
+
+ handleDeleteIssue(issue)}> +
+ + Delete issue +
+
+ +
+ + Copy issue link +
+
+
+ )} +
+ + ); +}; diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx new file mode 100644 index 000000000..9f615f165 --- /dev/null +++ b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx @@ -0,0 +1,241 @@ +import React from "react"; +// hooks +import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; +import useLocalStorage from "hooks/use-local-storage"; +// component +import { CustomMenu, Icon } from "components/ui"; +// icon +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +// types +import { TIssueOrderByOptions } from "types"; + +type Props = { + columnData: any; + gridTemplateColumns: string; +}; + +export const SpreadsheetColumns: React.FC = ({ columnData, gridTemplateColumns }) => { + const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage( + "spreadsheetViewSorting", + "" + ); + + const { orderBy, setOrderBy } = useSpreadsheetIssuesView(); + + const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => { + setOrderBy(order); + setSelectedMenuItem(`${order}_${itemKey}`); + }; + return ( +
+ {columnData.map((col: any) => { + if (col.isActive) { + return ( +
+ {col.propertyName === "title" || col.propertyName === "priority" ? ( +
+ {col.icon ? ( +
+ ) : ( + + {col.icon ? ( +
+ } + menuItemsWhiteBg + width="xl" + > + { + handleOrderBy(col.ascendingOrder, col.propertyName); + }} + > +
+
+ {col.propertyName === "assignee" || col.propertyName === "labels" ? ( + <> + A-Z + Ascending + + ) : col.propertyName === "due_date" ? ( + <> + 1-9 + Ascending + + ) : col.propertyName === "estimate" ? ( + <> + 0 + + 10 + + ) : ( + <> + First + + Last + + )} +
+ + +
+
+ { + handleOrderBy(col.descendingOrder, col.propertyName); + }} + > +
+
+ {col.propertyName === "assignee" || col.propertyName === "labels" ? ( + <> + Z-A + Descending + + ) : col.propertyName === "due_date" ? ( + <> + 9-1 + Descending + + ) : col.propertyName === "estimate" ? ( + <> + 10 + + 0 + + ) : ( + <> + Last + + First + + )} +
+ + +
+
+ { + handleOrderBy("-created_at", col.propertyName); + }} + > +
+
+ + None +
+ + +
+
+ + )} +
+ ); + } + })} + + ); +}; diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx new file mode 100644 index 000000000..8652b3a7e --- /dev/null +++ b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx @@ -0,0 +1,90 @@ +import React, { useState } from "react"; + +// components +import { SingleSpreadsheetIssue } from "components/core"; +// hooks +import useSubIssue from "hooks/use-sub-issue"; +// types +import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; + +type Props = { + key: string; + issue: IIssue; + expandedIssues: string[]; + setExpandedIssues: React.Dispatch>; + properties: Properties; + handleEditIssue: (issue: IIssue) => void; + handleDeleteIssue: (issue: IIssue) => void; + gridTemplateColumns: string; + user: ICurrentUserResponse | undefined; + userAuth: UserAuth; + nestingLevel?: number; +}; + +export const SpreadsheetIssues: React.FC = ({ + key, + issue, + expandedIssues, + setExpandedIssues, + gridTemplateColumns, + properties, + handleEditIssue, + handleDeleteIssue, + user, + userAuth, + nestingLevel = 0, +}) => { + const handleToggleExpand = (issueId: string) => { + setExpandedIssues((prevState) => { + const newArray = [...prevState]; + const index = newArray.indexOf(issueId); + if (index > -1) { + newArray.splice(index, 1); + } else { + newArray.push(issueId); + } + return newArray; + }); + }; + + const isExpanded = expandedIssues.indexOf(issue.id) > -1; + + const { subIssues, isLoading } = useSubIssue(issue.id, isExpanded); + + return ( +
+ handleEditIssue(issue)} + handleDeleteIssue={handleDeleteIssue} + user={user} + userAuth={userAuth} + nestingLevel={nestingLevel} + /> + + {isExpanded && + !isLoading && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssue: IIssue, subIndex: number) => ( + handleEditIssue(subIssue)} + handleDeleteIssue={handleDeleteIssue} + user={user} + userAuth={userAuth} + nestingLevel={nestingLevel + 1} + /> + ))} +
+ ); +}; diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx new file mode 100644 index 000000000..6f36b2dbb --- /dev/null +++ b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx @@ -0,0 +1,94 @@ +import React, { useState } from "react"; + +// next +import { useRouter } from "next/router"; + +// components +import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; +import { Icon, Spinner } from "components/ui"; +// hooks +import useIssuesProperties from "hooks/use-issue-properties"; +import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; +// types +import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; +// constants +import { SPREADSHEET_COLUMN } from "constants/spreadsheet"; +// icon +import { PlusIcon } from "@heroicons/react/24/outline"; + +type Props = { + handleEditIssue: (issue: IIssue) => void; + handleDeleteIssue: (issue: IIssue) => void; + user: ICurrentUserResponse | undefined; + userAuth: UserAuth; +}; + +export const SpreadsheetView: React.FC = ({ + handleEditIssue, + handleDeleteIssue, + user, + userAuth, +}) => { + const [expandedIssues, setExpandedIssues] = useState([]); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { spreadsheetIssues } = useSpreadsheetIssuesView(); + + const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); + + const columnData = SPREADSHEET_COLUMN.map((column) => ({ + ...column, + isActive: properties + ? column.propertyName === "labels" + ? properties[column.propertyName as keyof Properties] + : column.propertyName === "title" + ? true + : properties[column.propertyName as keyof Properties] + : false, + })); + + const gridTemplateColumns = columnData + .filter((column) => column.isActive) + .map((column) => column.colSize) + .join(" "); + + return ( +
+
+ +
+ {spreadsheetIssues ? ( +
+ {spreadsheetIssues.map((issue: IIssue, index) => ( + + ))} + +
+ ) : ( + + )} +
+ ); +}; diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx index d0ef4b6e9..ffdebb314 100644 --- a/apps/app/components/issues/delete-issue-modal.tsx +++ b/apps/app/components/issues/delete-issue-modal.tsx @@ -12,6 +12,7 @@ import issueServices from "services/issues.service"; import useIssuesView from "hooks/use-issues-view"; import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useToast from "hooks/use-toast"; +import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui @@ -41,6 +42,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u const { issueView, params } = useIssuesView(); const { params: calendarParams } = useCalendarIssuesView(); + const { params: spreadsheetParams } = useSpreadsheetIssuesView(); const { setToastAlert } = useToast(); @@ -74,6 +76,20 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u (prevData) => (prevData ?? []).filter((p) => p.id !== data.id), false ); + } else if (issueView === "spreadsheet") { + const spreadsheetFetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) + : viewId + ? VIEW_ISSUES(viewId.toString(), spreadsheetParams) + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams); + + mutate( + spreadsheetFetchKey, + (prevData) => (prevData ?? []).filter((p) => p.id !== data.id), + false + ); } else { if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index 22a275fee..d88ad674d 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -17,6 +17,7 @@ import useIssuesView from "hooks/use-issues-view"; import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useToast from "hooks/use-toast"; import useInboxView from "hooks/use-inbox-view"; +import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; // components import { IssueForm } from "components/issues"; // types @@ -79,6 +80,7 @@ export const CreateUpdateIssueModal: React.FC = ({ const { params: calendarParams } = useCalendarIssuesView(); const { order_by, group_by, ...viewGanttParams } = params; const { params: inboxParams } = useInboxView(); + const { params: spreadsheetParams } = useSpreadsheetIssuesView(); if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string }; if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string }; @@ -211,6 +213,14 @@ export const CreateUpdateIssueModal: React.FC = ({ ? VIEW_ISSUES(viewId.toString(), calendarParams) : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams); + const spreadsheetFetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) + : viewId + ? VIEW_ISSUES(viewId.toString(), spreadsheetParams) + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams); + const ganttFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) : moduleId @@ -234,6 +244,7 @@ export const CreateUpdateIssueModal: React.FC = ({ if (issueView === "calendar") mutate(calendarFetchKey); if (issueView === "gantt_chart") mutate(ganttFetchKey); + if (issueView === "spreadsheet") mutate(spreadsheetFetchKey); setToastAlert({ type: "success", @@ -264,6 +275,7 @@ export const CreateUpdateIssueModal: React.FC = ({ mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); } else { if (issueView === "calendar") mutate(calendarFetchKey); + if (issueView === "spreadsheet") mutate(spreadsheetFetchKey); mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); } diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx index 27d4901f6..1dbfbabba 100644 --- a/apps/app/components/issues/view-select/assignee.tsx +++ b/apps/app/components/issues/view-select/assignee.tsx @@ -22,6 +22,7 @@ type Props = { position?: "left" | "right"; selfPositioned?: boolean; tooltipPosition?: "left" | "right"; + customButton?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -34,6 +35,7 @@ export const ViewAssigneeSelect: React.FC = ({ tooltipPosition = "right", user, isNotAllowed, + customButton = false, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -65,6 +67,38 @@ export const ViewAssigneeSelect: React.FC = ({ ), })); + const assigneeLabel = ( + 0 + ? issue.assignee_details + .map((assignee) => + assignee?.first_name !== "" ? assignee?.first_name : assignee?.email + ) + .join(", ") + : "No Assignee" + } + > +
+ {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ ); + return ( = ({ ); }} options={options} - label={ - 0 - ? issue.assignee_details - .map((assignee) => - assignee?.first_name !== "" ? assignee?.first_name : assignee?.email - ) - .join(", ") - : "No Assignee" - } - > -
- {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? ( -
- -
- ) : ( -
- -
- )} -
-
- } + {...(customButton ? { customButton: assigneeLabel } : { label: assigneeLabel })} multiple noChevron position={position} diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index bea5ff045..f74b62689 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -12,6 +12,7 @@ import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issueId: string) => void; + noBorder?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -19,6 +20,7 @@ type Props = { export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, + noBorder = false, user, isNotAllowed, }) => { @@ -62,6 +64,7 @@ export const ViewDueDateSelect: React.FC = ({ ); }} className={issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"} + noBorder={noBorder} disabled={isNotAllowed} /> diff --git a/apps/app/components/issues/view-select/estimate.tsx b/apps/app/components/issues/view-select/estimate.tsx index 914a5286e..02a3e0710 100644 --- a/apps/app/components/issues/view-select/estimate.tsx +++ b/apps/app/components/issues/view-select/estimate.tsx @@ -18,6 +18,7 @@ type Props = { partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + customButton?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -27,6 +28,7 @@ export const ViewEstimateSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + customButton = false, user, isNotAllowed, }) => { @@ -37,6 +39,15 @@ export const ViewEstimateSelect: React.FC = ({ const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value; + const estimateLabels = ( + +
+ + {estimateValue ?? "None"} +
+
+ ); + if (!isEstimateActive) return null; return ( @@ -57,14 +68,7 @@ export const ViewEstimateSelect: React.FC = ({ user ); }} - label={ - -
- - {estimateValue ?? "Estimate"} -
-
- } + {...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })} maxHeight="md" noChevron disabled={isNotAllowed} diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index a0c5cd47c..499546931 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -12,12 +12,15 @@ import { ICurrentUserResponse, IIssue } from "types"; import { PRIORITIES } from "constants/project"; // services import trackEventServices from "services/track-event.service"; +// helper +import { capitalizeFirstLetter } from "helpers/string.helper"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + noBorder?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -27,6 +30,7 @@ export const ViewPrioritySelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + noBorder = false, user, isNotAllowed, }) => { @@ -55,10 +59,12 @@ export const ViewPrioritySelect: React.FC = ({ customButton={ diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 2b904eb1e..c097c7326 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -22,6 +22,7 @@ type Props = { partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + customButton?: boolean; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -31,6 +32,7 @@ export const ViewStateSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + customButton = false, user, isNotAllowed, }) => { @@ -58,6 +60,19 @@ export const ViewStateSelect: React.FC = ({ const selectedOption = states?.find((s) => s.id === issue.state); + const stateLabel = ( + +
+ {selectedOption && + getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)} + {selectedOption?.name ?? "State"} +
+
+ ); + return ( = ({ } }} options={options} - label={ - -
- {selectedOption && - getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)} - {selectedOption?.name ?? "State"} -
-
- } + {...(customButton ? { customButton: stateLabel } : { label: stateLabel })} position={position} disabled={isNotAllowed} noChevron diff --git a/apps/app/components/ui/custom-menu.tsx b/apps/app/components/ui/custom-menu.tsx index dac7927b1..006802b79 100644 --- a/apps/app/components/ui/custom-menu.tsx +++ b/apps/app/components/ui/custom-menu.tsx @@ -20,6 +20,7 @@ type Props = { position?: "left" | "right"; verticalPosition?: "top" | "bottom"; customButton?: JSX.Element; + menuItemsWhiteBg?: boolean; }; type MenuItemProps = { @@ -44,6 +45,7 @@ const CustomMenu = ({ position = "right", verticalPosition = "bottom", customButton, + menuItemsWhiteBg = false, }: Props) => ( {({ open }) => ( @@ -105,7 +107,7 @@ const CustomMenu = ({ leaveTo="transform opacity-0 scale-95" >
{children}
diff --git a/apps/app/components/ui/datepicker.tsx b/apps/app/components/ui/datepicker.tsx index 999b46ce4..80ce7aa91 100644 --- a/apps/app/components/ui/datepicker.tsx +++ b/apps/app/components/ui/datepicker.tsx @@ -11,6 +11,7 @@ type Props = { placeholder?: string; displayShortForm?: boolean; error?: boolean; + noBorder?: boolean; className?: string; isClearable?: boolean; disabled?: boolean; @@ -23,6 +24,7 @@ export const CustomDatePicker: React.FC = ({ placeholder = "Select date", displayShortForm = false, error = false, + noBorder = false, className = "", isClearable = true, disabled = false, @@ -44,7 +46,9 @@ export const CustomDatePicker: React.FC = ({ : "" } ${error ? "border-red-500 bg-red-100" : ""} ${ disabled ? "cursor-not-allowed" : "cursor-pointer" - } w-full rounded-md border border-brand-base bg-transparent caret-transparent ${className}`} + } ${ + noBorder ? "" : "border border-brand-base" + } w-full rounded-md bg-transparent caret-transparent ${className}`} dateFormat="dd-MM-yyyy" isClearable={isClearable} disabled={disabled} diff --git a/apps/app/components/ui/multi-level-dropdown.tsx b/apps/app/components/ui/multi-level-dropdown.tsx index 0033e8e02..0f25d06b3 100644 --- a/apps/app/components/ui/multi-level-dropdown.tsx +++ b/apps/app/components/ui/multi-level-dropdown.tsx @@ -35,7 +35,7 @@ export const MultiLevelDropdown: React.FC = ({ const [openChildFor, setOpenChildFor] = useState(null); return ( - + {({ open }) => ( <>
diff --git a/apps/app/components/ui/tooltip.tsx b/apps/app/components/ui/tooltip.tsx index 11504facd..86ca39e54 100644 --- a/apps/app/components/ui/tooltip.tsx +++ b/apps/app/components/ui/tooltip.tsx @@ -42,7 +42,7 @@ export const Tooltip: React.FC = ({ disabled={disabled} content={
diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 5625b710d..7e77e6dc2 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -1,7 +1,7 @@ import { IAnalyticsParams, IJiraMetadata } from "types"; const paramsToKey = (params: any) => { - const { state, priority, assignees, created_by, labels, target_date } = params; + const { state, priority, assignees, created_by, labels, target_date, sub_issue } = params; let stateKey = state ? state.split(",") : []; let priorityKey = priority ? priority.split(",") : []; @@ -12,6 +12,7 @@ const paramsToKey = (params: any) => { 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"; + const subIssue = sub_issue ? sub_issue.toUpperCase() : "NULL"; // sorting each keys in ascending order stateKey = stateKey.sort().join("_"); @@ -20,7 +21,7 @@ const paramsToKey = (params: any) => { createdByKey = createdByKey.sort().join("_"); labelsKey = labelsKey.sort().join("_"); - return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`; + return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${subIssue}`; }; const inboxParamsToKey = (params: any) => { diff --git a/apps/app/constants/spreadsheet.ts b/apps/app/constants/spreadsheet.ts new file mode 100644 index 000000000..5ef60f40d --- /dev/null +++ b/apps/app/constants/spreadsheet.ts @@ -0,0 +1,60 @@ +import { + CalendarDaysIcon, + PlayIcon, + Squares2X2Icon, + TagIcon, + UserGroupIcon, +} from "@heroicons/react/24/outline"; + +export const SPREADSHEET_COLUMN = [ + { + propertyName: "title", + colName: "Title", + colSize: "440px", + }, + { + propertyName: "state", + colName: "State", + colSize: "128px", + icon: Squares2X2Icon, + ascendingOrder: "state__name", + descendingOrder: "-state__name", + }, + { + propertyName: "priority", + colName: "Priority", + colSize: "128px", + }, + { + propertyName: "assignee", + colName: "Assignees", + colSize: "128px", + icon: UserGroupIcon, + ascendingOrder: "assignees__name", + descendingOrder: "-assignees__name", + }, + { + propertyName: "labels", + colName: "Labels", + colSize: "128px", + icon: TagIcon, + ascendingOrder: "labels__name", + descendingOrder: "-labels__name", + }, + { + propertyName: "due_date", + colName: "Due Date", + colSize: "128px", + icon: CalendarDaysIcon, + ascendingOrder: "target_date", + descendingOrder: "-target_date", + }, + { + propertyName: "estimate", + colName: "Estimate", + colSize: "128px", + icon: PlayIcon, + ascendingOrder: "estimate_point", + descendingOrder: "-estimate_point", + }, +]; diff --git a/apps/app/hooks/use-spreadsheet-issues-view.tsx b/apps/app/hooks/use-spreadsheet-issues-view.tsx new file mode 100644 index 000000000..6e7b66bec --- /dev/null +++ b/apps/app/hooks/use-spreadsheet-issues-view.tsx @@ -0,0 +1,125 @@ +import { useContext } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// contexts +import { issueViewContext } from "contexts/issue-view.context"; +// services +import issuesService from "services/issues.service"; +import cyclesService from "services/cycles.service"; +import modulesService from "services/modules.service"; +// types +import { IIssue } from "types"; +// fetch-keys +import { + CYCLE_ISSUES_WITH_PARAMS, + MODULE_ISSUES_WITH_PARAMS, + PROJECT_ISSUES_LIST_WITH_PARAMS, + VIEW_ISSUES, +} from "constants/fetch-keys"; + +const useSpreadsheetIssuesView = () => { + const { + issueView, + orderBy, + setOrderBy, + filters, + setFilters, + resetFilterToDefault, + setNewFilterDefaultView, + setIssueView, + } = useContext(issueViewContext); + + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; + + const params: any = { + order_by: orderBy, + assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, + state: filters?.state ? filters?.state.join(",") : undefined, + 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", + }; + + const { data: projectSpreadsheetIssues } = useSWR( + workspaceSlug && projectId + ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params) + : null, + workspaceSlug && projectId + ? () => + issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params) + : null + ); + + const { data: cycleSpreadsheetIssues } = useSWR( + workspaceSlug && projectId && cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) + : null, + workspaceSlug && projectId && cycleId + ? () => + cyclesService.getCycleIssuesWithParams( + workspaceSlug.toString(), + projectId.toString(), + cycleId.toString(), + params + ) + : null + ); + + const { data: moduleSpreadsheetIssues } = useSWR( + workspaceSlug && projectId && moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) + : null, + workspaceSlug && projectId && moduleId + ? () => + modulesService.getModuleIssuesWithParams( + workspaceSlug.toString(), + projectId.toString(), + moduleId.toString(), + params + ) + : null + ); + + const { data: viewSpreadsheetIssues } = useSWR( + workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null, + workspaceSlug && projectId && viewId && params + ? () => + issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params) + : null + ); + + const spreadsheetIssues = cycleId + ? (cycleSpreadsheetIssues as IIssue[]) + : moduleId + ? (moduleSpreadsheetIssues as IIssue[]) + : viewId + ? (viewSpreadsheetIssues as IIssue[]) + : (projectSpreadsheetIssues as IIssue[]); + + return { + issueView, + spreadsheetIssues: spreadsheetIssues ?? [], + orderBy, + setOrderBy, + filters, + setFilters, + params, + resetFilterToDefault, + setNewFilterDefaultView, + setIssueView, + } as const; +}; + +export default useSpreadsheetIssuesView; diff --git a/apps/app/hooks/use-sub-issue.tsx b/apps/app/hooks/use-sub-issue.tsx new file mode 100644 index 000000000..8eb30fd0b --- /dev/null +++ b/apps/app/hooks/use-sub-issue.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import issuesService from "services/issues.service"; +// types +import { ISubIssueResponse } from "types"; +// fetch-keys +import { SUB_ISSUES } from "constants/fetch-keys"; + +const useSubIssue = (issueId: string, isExpanded: boolean) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const shouldFetch = workspaceSlug && projectId && issueId && isExpanded; + + const { data: subIssuesResponse, isLoading } = useSWR( + shouldFetch ? SUB_ISSUES(issueId as string) : null, + shouldFetch + ? () => + issuesService.subIssues(workspaceSlug as string, projectId as string, issueId as string) + : null + ); + + return { + subIssues: subIssuesResponse?.sub_issues ?? [], + isLoading, + }; +}; + +export default useSubIssue; diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index a8881924b..a33a04ffc 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -247,11 +247,25 @@ export interface IIssueFilterOptions { created_by: string[] | null; } -export type TIssueViewOptions = "list" | "kanban" | "calendar" | "gantt_chart"; +export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null; -export type TIssueOrderByOptions = "-created_at" | "-updated_at" | "priority" | "sort_order"; +export type TIssueOrderByOptions = + | "-created_at" + | "-updated_at" + | "priority" + | "sort_order" + | "state__name" + | "-state__name" + | "assignees__name" + | "-assignees__name" + | "labels__name" + | "-labels__name" + | "target_date" + | "-target_date" + | "estimate__point" + | "-estimate__point"; export interface IIssueViewOptions { group_by: TIssueGroupByOptions;