import React, { useCallback, useState } from "react"; // next import { useRouter } from "next/router"; import { KeyedMutator, mutate } from "swr"; // components import { ListInlineCreateIssueForm, SpreadsheetAssigneeColumn, SpreadsheetCreatedOnColumn, SpreadsheetDueDateColumn, SpreadsheetEstimateColumn, SpreadsheetIssuesColumn, SpreadsheetLabelColumn, SpreadsheetPriorityColumn, SpreadsheetStartDateColumn, SpreadsheetStateColumn, SpreadsheetUpdatedOnColumn, } from "components/core"; import { CustomMenu, Spinner } from "components/ui"; import { IssuePeekOverview } from "components/issues"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; // types import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types"; import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter"; import { CYCLE_DETAILS, CYCLE_ISSUES_WITH_PARAMS, MODULE_DETAILS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, SUB_ISSUES, VIEW_ISSUES, WORKSPACE_VIEW_ISSUES, } from "constants/fetch-keys"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import projectIssuesServices from "services/issues.service"; // icon import { PlusIcon } from "lucide-react"; type Props = { spreadsheetIssues: IIssue[]; mutateIssues: KeyedMutator< | IIssue[] | { [key: string]: IIssue[]; } >; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; openIssuesListModal?: (() => void) | null; disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; export const SpreadsheetView: React.FC<Props> = ({ spreadsheetIssues, mutateIssues, handleIssueAction, openIssuesListModal, disableUserActions, user, userAuth, }) => { const [expandedIssues, setExpandedIssues] = useState<string[]>([]); const [currentProjectId, setCurrentProjectId] = useState<string | null>(null); const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query; const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const workspaceIssuesPath = [ { params: { sub_issue: false, }, path: "workspace-views/all-issues", }, { params: { assignees: user?.id ?? undefined, sub_issue: false, }, path: "workspace-views/assigned", }, { params: { created_by: user?.id ?? undefined, sub_issue: false, }, path: "workspace-views/created", }, { params: { subscriber: user?.id ?? undefined, sub_issue: false, }, path: "workspace-views/subscribed", }, ]; const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) => router.pathname.includes(path.path) ); const { params: workspaceViewParams } = useWorkspaceIssuesFilters( workspaceSlug?.toString(), workspaceViewId?.toString() ); const { params } = useSpreadsheetIssuesView(); const partialUpdateIssue = useCallback( (formData: Partial<IIssue>, issue: IIssue) => { if (!workspaceSlug || !issue) return; const fetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) : moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : viewId ? VIEW_ISSUES(viewId.toString(), params) : workspaceViewId ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), workspaceViewParams) : currentWorkspaceIssuePath ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params) : PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project_detail.id, params); if (issue.parent) mutate<ISubIssueResponse>( SUB_ISSUES(issue.parent.toString()), (prevData) => { if (!prevData) return prevData; return { ...prevData, sub_issues: (prevData.sub_issues ?? []).map((i) => { if (i.id === issue.id) { return { ...i, ...formData, }; } return i; }), }; }, false ); else mutate<IIssue[]>( fetchKey, (prevData) => (prevData ?? []).map((p) => { if (p.id === issue.id) { return { ...p, ...formData, }; } return p; }), false ); projectIssuesServices .patchIssue( workspaceSlug as string, issue.project_detail.id, issue.id as string, formData, user ) .then(() => { if (issue.parent) { mutate(SUB_ISSUES(issue.parent as string)); } else { mutate(fetchKey); if (cycleId) mutate(CYCLE_DETAILS(cycleId as string)); if (moduleId) mutate(MODULE_DETAILS(moduleId as string)); } }) .catch((error) => { console.log(error); }); }, [ workspaceSlug, cycleId, moduleId, viewId, workspaceViewId, currentWorkspaceIssuePath, workspaceViewParams, params, user, ] ); const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const renderColumn = (header: string, Component: React.ComponentType<any>) => ( <div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm"> <div className="flex items-center min-w-[9rem] px-4 py-2.5 text-sm font-medium z-[1] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200"> {header} </div> <div className="h-full min-w-[9rem] w-full"> {spreadsheetIssues.map((issue: IIssue, index) => ( <Component key={`${issue.id}_${index}`} issue={issue} projectId={issue.project_detail.id} partialUpdateIssue={partialUpdateIssue} expandedIssues={expandedIssues} properties={properties} user={user} isNotAllowed={isNotAllowed} /> ))} </div> </div> ); return ( <> <IssuePeekOverview handleMutation={() => mutateIssues()} projectId={currentProjectId ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} readOnly={disableUserActions} /> <div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100"> <div className="h-full w-full flex flex-col"> <div className="flex max-h-full overflow-y-auto"> {spreadsheetIssues ? ( <> <div className="sticky left-0 w-[28rem] z-[2]"> <div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm z-[2]"> <div className="flex items-center text-sm font-medium z-[2] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200"> <span className="flex items-center px-4 py-2.5 h-full w-20 flex-shrink-0"> ID </span> <span className="flex items-center px-4 py-2.5 h-full w-full flex-grow"> Issue </span> </div> {spreadsheetIssues.map((issue: IIssue, index) => ( <SpreadsheetIssuesColumn key={`${issue.id}_${index}`} issue={issue} projectId={issue.project_detail.id} expandedIssues={expandedIssues} setExpandedIssues={setExpandedIssues} setCurrentProjectId={setCurrentProjectId} properties={properties} handleIssueAction={handleIssueAction} disableUserActions={disableUserActions} userAuth={userAuth} /> ))} </div> </div> {renderColumn("State", SpreadsheetStateColumn)} {renderColumn("Priority", SpreadsheetPriorityColumn)} {renderColumn("Assignees", SpreadsheetAssigneeColumn)} {renderColumn("Label", SpreadsheetLabelColumn)} {renderColumn("Start Date", SpreadsheetStartDateColumn)} {renderColumn("Due Date", SpreadsheetDueDateColumn)} {renderColumn("Estimate", SpreadsheetEstimateColumn)} {renderColumn("Created On", SpreadsheetCreatedOnColumn)} {renderColumn("Updated On", SpreadsheetUpdatedOnColumn)} </> ) : ( <div className="flex flex-col justify-center items-center h-full w-full"> <Spinner /> </div> )} </div> <div className="border-t border-custom-border-100"> <div className="mb-3 z-50 sticky bottom-0 left-0"> <ListInlineCreateIssueForm isOpen={isInlineCreateIssueFormOpen} handleClose={() => setIsInlineCreateIssueFormOpen(false)} prePopulatedData={{ ...(cycleId && { cycle: cycleId.toString() }), ...(moduleId && { module: moduleId.toString() }), }} /> </div> {type === "issue" ? !disableUserActions && !isInlineCreateIssueFormOpen && ( <button className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] w-full" onClick={() => setIsInlineCreateIssueFormOpen(true)} > <PlusIcon className="h-4 w-4" /> New Issue </button> ) : !disableUserActions && !isInlineCreateIssueFormOpen && ( <CustomMenu className="sticky left-0 z-10" customButton={ <button className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full" type="button" > <PlusIcon className="h-4 w-4" /> New Issue </button> } position="left" verticalPosition="top" optionsClassName="left-5 !w-36" noBorder > <CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}> Create new </CustomMenu.MenuItem> {openIssuesListModal && ( <CustomMenu.MenuItem onClick={openIssuesListModal}> Add an existing issue </CustomMenu.MenuItem> )} </CustomMenu> )} </div> </div> </div> </> ); };