dev: revamp peek overview (#2021)

* dev: mobx for issues store

* refactor: peek overview component

* chore: update open issue button

* fix: issue mutation after any crud action

* chore: remove peek overview from gantt

* chore: refactor code
This commit is contained in:
Aaryan Khandelwal 2023-08-30 13:26:28 +05:30 committed by GitHub
parent 17aff1f369
commit f5a076e9a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 576 additions and 274 deletions

View File

@ -665,7 +665,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center gap-2 text-custom-text-200"> <div className="flex items-center gap-2 text-custom-text-200">
<DiscordIcon className="h-4 w-4" color="#6b7280" /> <DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
Join our Discord Join our Discord
</div> </div>
</Command.Item> </Command.Item>

View File

@ -6,7 +6,6 @@ import { mutate } from "swr";
// components // components
import { import {
IssuePeekOverview,
ViewAssigneeSelect, ViewAssigneeSelect,
ViewDueDateSelect, ViewDueDateSelect,
ViewEstimateSelect, ViewEstimateSelect,
@ -76,9 +75,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
// issue peek overview
const [issuePeekOverview, setIssuePeekOverview] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@ -161,6 +157,15 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user] [workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
); );
const openPeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const handleCopyText = () => { const handleCopyText = () => {
const originURL = const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
@ -183,15 +188,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
return ( return (
<> <>
<IssuePeekOverview
handleDeleteIssue={() => handleDeleteIssue(issue)}
handleUpdateIssue={async (formData) => partialUpdateIssue(formData, issue)}
issue={issue}
isOpen={issuePeekOverview}
onClose={() => setIssuePeekOverview(false)}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={isNotAllowed}
/>
<div <div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max" className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }} style={{ gridTemplateColumns }}
@ -280,7 +276,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
<button <button
type="button" type="button"
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]" className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
onClick={() => setIssuePeekOverview(true)} onClick={openPeekOverview}
> >
{issue.name} {issue.name}
</button> </button>

View File

@ -6,6 +6,7 @@ import { useRouter } from "next/router";
// components // components
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
import { CustomMenu, Spinner } from "components/ui"; import { CustomMenu, Spinner } from "components/ui";
import { IssuePeekOverview } from "components/issues";
// hooks // hooks
import useIssuesProperties from "hooks/use-issue-properties"; import useIssuesProperties from "hooks/use-issue-properties";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
@ -38,7 +39,7 @@ export const SpreadsheetView: React.FC<Props> = ({
const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { spreadsheetIssues } = useSpreadsheetIssuesView(); const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
@ -59,80 +60,88 @@ export const SpreadsheetView: React.FC<Props> = ({
.join(" "); .join(" ");
return ( return (
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100"> <>
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max"> <IssuePeekOverview
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} /> handleMutation={() => mutateIssues()}
</div> projectId={projectId?.toString() ?? ""}
{spreadsheetIssues ? ( workspaceSlug={workspaceSlug?.toString() ?? ""}
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm "> readOnly={disableUserActions}
{spreadsheetIssues.map((issue: IIssue, index) => ( />
<SpreadsheetIssues <div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
key={`${issue.id}_${index}`} <div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
index={index} <SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
issue={issue}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={properties}
handleIssueAction={handleIssueAction}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
/>
))}
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
{type === "issue" ? (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
!disableUserActions && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)
)}
</div>
</div> </div>
) : ( {spreadsheetIssues ? (
<Spinner /> <div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
)} {spreadsheetIssues.map((issue: IIssue, index) => (
</div> <SpreadsheetIssues
key={`${issue.id}_${index}`}
index={index}
issue={issue}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={properties}
handleIssueAction={handleIssueAction}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
/>
))}
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
{type === "issue" ? (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
!disableUserActions && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)
)}
</div>
</div>
) : (
<Spinner />
)}
</div>
</>
); );
}; };

View File

@ -15,7 +15,7 @@ export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
return ( return (
<div <div
className="flex items-center relative h-full w-full rounded" className="flex items-center relative h-full w-full rounded cursor-pointer"
style={{ backgroundColor: data?.state_detail?.color }} style={{ backgroundColor: data?.state_detail?.color }}
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
> >
@ -49,7 +49,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
return ( return (
<div <div
className="relative w-full flex items-center gap-2 h-full" className="relative w-full flex items-center gap-2 h-full cursor-pointer"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
> >
{getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)} {getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)}

View File

@ -1,3 +1,4 @@
// components
import { import {
PeekOverviewHeader, PeekOverviewHeader,
PeekOverviewIssueActivity, PeekOverviewIssueActivity,
@ -5,13 +6,16 @@ import {
PeekOverviewIssueProperties, PeekOverviewIssueProperties,
TPeekOverviewModes, TPeekOverviewModes,
} from "components/issues"; } from "components/issues";
// ui
import { Loader } from "components/ui";
// types
import { IIssue } from "types"; import { IIssue } from "types";
type Props = { type Props = {
handleClose: () => void; handleClose: () => void;
handleDeleteIssue: () => void; handleDeleteIssue: () => void;
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>; handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
issue: IIssue; issue: IIssue | undefined;
mode: TPeekOverviewModes; mode: TPeekOverviewModes;
readOnly: boolean; readOnly: boolean;
setMode: (mode: TPeekOverviewModes) => void; setMode: (mode: TPeekOverviewModes) => void;
@ -40,39 +44,59 @@ export const FullScreenPeekView: React.FC<Props> = ({
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
/> />
</div> </div>
<div className="h-full w-full px-6 overflow-y-auto"> {issue ? (
{/* issue title and description */} <div className="h-full w-full px-6 overflow-y-auto">
<div className="w-full"> {/* issue title and description */}
<PeekOverviewIssueDetails <div className="w-full">
handleUpdateIssue={handleUpdateIssue} <PeekOverviewIssueDetails
issue={issue} handleUpdateIssue={handleUpdateIssue}
readOnly={readOnly} issue={issue}
workspaceSlug={workspaceSlug} readOnly={readOnly}
/> workspaceSlug={workspaceSlug}
/>
</div>
{/* divider */}
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
{/* issue activity/comments */}
<div className="w-full pb-5">
<PeekOverviewIssueActivity
workspaceSlug={workspaceSlug}
issue={issue}
readOnly={readOnly}
/>
</div>
</div> </div>
{/* divider */} ) : (
<div className="h-[1] w-full border-t border-custom-border-200 my-5" /> <Loader className="px-6">
{/* issue activity/comments */} <Loader.Item height="30px" />
<div className="w-full"> <div className="space-y-2 mt-3">
<PeekOverviewIssueActivity <Loader.Item height="20px" width="70%" />
workspaceSlug={workspaceSlug} <Loader.Item height="20px" width="60%" />
issue={issue} <Loader.Item height="20px" width="60%" />
readOnly={readOnly} </div>
/> </Loader>
</div> )}
</div>
</div> </div>
<div className="col-span-3 h-full w-full overflow-y-auto"> <div className="col-span-3 h-full w-full overflow-y-auto">
{/* issue properties */} {/* issue properties */}
<div className="w-full px-6 py-5"> <div className="w-full px-6 py-5">
<PeekOverviewIssueProperties {issue ? (
handleDeleteIssue={handleDeleteIssue} <PeekOverviewIssueProperties
issue={issue} handleDeleteIssue={handleDeleteIssue}
mode="full" handleUpdateIssue={handleUpdateIssue}
onChange={handleUpdateIssue} issue={issue}
readOnly={readOnly} mode="full"
workspaceSlug={workspaceSlug} readOnly={readOnly}
/> workspaceSlug={workspaceSlug}
/>
) : (
<Loader className="mt-11 space-y-4">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,18 +1,21 @@
import Link from "next/link";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { CustomSelect, Icon } from "components/ui"; import { CustomSelect, Icon } from "components/ui";
// icons
import { East, OpenInFull } from "@mui/icons-material";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { TPeekOverviewModes } from "./layout"; import { TPeekOverviewModes } from "./layout";
import { ArrowRightAlt, CloseFullscreen, East, OpenInFull } from "@mui/icons-material";
type Props = { type Props = {
handleClose: () => void; handleClose: () => void;
handleDeleteIssue: () => void; handleDeleteIssue: () => void;
issue: IIssue; issue: IIssue | undefined;
mode: TPeekOverviewModes; mode: TPeekOverviewModes;
setMode: (mode: TPeekOverviewModes) => void; setMode: (mode: TPeekOverviewModes) => void;
workspaceSlug: string; workspaceSlug: string;
@ -47,12 +50,9 @@ export const PeekOverviewHeader: React.FC<Props> = ({
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const handleCopyLink = () => { const handleCopyLink = () => {
const originURL = const urlToCopy = window.location.href;
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard( copyTextToClipboard(urlToCopy).then(() => {
`${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
).then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Link copied!", title: "Link copied!",
@ -73,23 +73,15 @@ export const PeekOverviewHeader: React.FC<Props> = ({
/> />
</button> </button>
)} )}
{mode === "modal" || mode === "full" ? ( <Link href={`/${workspaceSlug}/projects/${issue?.project}/issues/${issue?.id}`}>
<button type="button" onClick={() => setMode("side")}> <a>
<CloseFullscreen
sx={{
fontSize: "14px",
}}
/>
</button>
) : (
<button type="button" onClick={() => setMode("modal")}>
<OpenInFull <OpenInFull
sx={{ sx={{
fontSize: "14px", fontSize: "14px",
}} }}
/> />
</button> </a>
)} </Link>
<CustomSelect <CustomSelect
value={mode} value={mode}
onChange={(val: TPeekOverviewModes) => setMode(val)} onChange={(val: TPeekOverviewModes) => setMode(val)}
@ -119,7 +111,7 @@ export const PeekOverviewHeader: React.FC<Props> = ({
</CustomSelect> </CustomSelect>
</div> </div>
{(mode === "side" || mode === "modal") && ( {(mode === "side" || mode === "modal") && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-shrink-0">
<button type="button" onClick={handleCopyLink} className="-rotate-45"> <button type="button" onClick={handleCopyLink} className="-rotate-45">
<Icon iconName="link" /> <Icon iconName="link" />
</button> </button>

View File

@ -1,6 +1,11 @@
// mobx
import { observer } from "mobx-react-lite";
// headless ui // headless ui
import { Disclosure } from "@headlessui/react"; import { Disclosure } from "@headlessui/react";
import { getStateGroupIcon } from "components/icons"; import { getStateGroupIcon } from "components/icons";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// components // components
import { import {
SidebarAssigneeSelect, SidebarAssigneeSelect,
@ -9,27 +14,27 @@ import {
SidebarStateSelect, SidebarStateSelect,
TPeekOverviewModes, TPeekOverviewModes,
} from "components/issues"; } from "components/issues";
// icons // ui
import { CustomDatePicker, Icon } from "components/ui"; import { CustomDatePicker, Icon } from "components/ui";
// helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
import useToast from "hooks/use-toast";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
type Props = { type Props = {
handleDeleteIssue: () => void; handleDeleteIssue: () => void;
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
issue: IIssue; issue: IIssue;
mode: TPeekOverviewModes; mode: TPeekOverviewModes;
onChange: (issueProperty: Partial<IIssue>) => void;
readOnly: boolean; readOnly: boolean;
workspaceSlug: string; workspaceSlug: string;
}; };
export const PeekOverviewIssueProperties: React.FC<Props> = ({ export const PeekOverviewIssueProperties: React.FC<Props> = ({
handleDeleteIssue, handleDeleteIssue,
handleUpdateIssue,
issue, issue,
mode, mode,
onChange,
readOnly, readOnly,
workspaceSlug, workspaceSlug,
}) => { }) => {
@ -86,7 +91,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
<div className="w-3/4"> <div className="w-3/4">
<SidebarStateSelect <SidebarStateSelect
value={issue.state} value={issue.state}
onChange={(val: string) => onChange({ state: val })} onChange={(val: string) => handleUpdateIssue({ state: val })}
disabled={readOnly} disabled={readOnly}
/> />
</div> </div>
@ -99,7 +104,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
<div className="w-3/4"> <div className="w-3/4">
<SidebarAssigneeSelect <SidebarAssigneeSelect
value={issue.assignees_list} value={issue.assignees_list}
onChange={(val: string[]) => onChange({ assignees_list: val })} onChange={(val: string[]) => handleUpdateIssue({ assignees_list: val })}
disabled={readOnly} disabled={readOnly}
/> />
</div> </div>
@ -112,7 +117,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
<div className="w-3/4"> <div className="w-3/4">
<SidebarPrioritySelect <SidebarPrioritySelect
value={issue.priority} value={issue.priority}
onChange={(val: string) => onChange({ priority: val })} onChange={(val: string) => handleUpdateIssue({ priority: val })}
disabled={readOnly} disabled={readOnly}
/> />
</div> </div>
@ -128,7 +133,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
placeholder="Start date" placeholder="Start date"
value={issue.start_date} value={issue.start_date}
onChange={(val) => onChange={(val) =>
onChange({ handleUpdateIssue({
start_date: val, start_date: val,
}) })
} }
@ -153,7 +158,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
placeholder="Due date" placeholder="Due date"
value={issue.target_date} value={issue.target_date}
onChange={(val) => onChange={(val) =>
onChange({ handleUpdateIssue({
target_date: val, target_date: val,
}) })
} }
@ -175,7 +180,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
<div className="w-3/4"> <div className="w-3/4">
<SidebarEstimateSelect <SidebarEstimateSelect
value={issue.estimate_point} value={issue.estimate_point}
onChange={(val: number | null) => onChange({ estimate_point: val })} onChange={(val: number | null) =>handleUpdateIssue({ estimate_point: val })}
disabled={readOnly} disabled={readOnly}
/> />
</div> </div>

View File

@ -1,107 +1,184 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// hooks
import useUser from "hooks/use-user";
// components
import { FullScreenPeekView, SidePeekView } from "components/issues"; import { FullScreenPeekView, SidePeekView } from "components/issues";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
type Props = { type Props = {
handleDeleteIssue: () => void; handleMutation: () => void;
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>; projectId: string;
issue: IIssue | null;
isOpen: boolean;
onClose: () => void;
workspaceSlug: string;
readOnly: boolean; readOnly: boolean;
workspaceSlug: string;
}; };
export type TPeekOverviewModes = "side" | "modal" | "full"; export type TPeekOverviewModes = "side" | "modal" | "full";
export const IssuePeekOverview: React.FC<Props> = ({ export const IssuePeekOverview: React.FC<Props> = observer(
handleDeleteIssue, ({ handleMutation, projectId, readOnly, workspaceSlug }) => {
handleUpdateIssue, const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
issue, const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
isOpen, const [peekOverviewMode, setPeekOverviewMode] = useState<TPeekOverviewModes>("side");
onClose,
workspaceSlug,
readOnly,
}) => {
const [peekOverviewMode, setPeekOverviewMode] = useState<TPeekOverviewModes>("side");
const handleClose = () => { const router = useRouter();
onClose(); const { peekIssue } = router.query;
setPeekOverviewMode("side");
};
if (!issue || !isOpen) return null; const { issues: issuesStore } = useMobxStore();
const { deleteIssue, getIssueById, issues, updateIssue } = issuesStore;
return ( const issue = issues[peekIssue?.toString() ?? ""];
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> const { user } = useUser();
{/* add backdrop conditionally */}
{(peekOverviewMode === "modal" || peekOverviewMode === "full") && ( const handleClose = () => {
<Transition.Child const { query } = router;
as={React.Fragment} delete query.peekIssue;
enter="ease-out duration-300"
enterFrom="opacity-0" router.push({
enterTo="opacity-100" pathname: router.pathname,
leave="ease-in duration-200" query: { ...query },
leaveFrom="opacity-100" });
leaveTo="opacity-0" };
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" /> const handleUpdateIssue = async (formData: Partial<IIssue>) => {
</Transition.Child> if (!issue || !user) return;
)}
<div className="fixed inset-0 z-20 overflow-y-auto"> await updateIssue(workspaceSlug, projectId, issue.id, formData, user);
<div className="relative h-full w-full"> handleMutation();
};
const handleDeleteIssue = async () => {
if (!issue || !user) return;
await deleteIssue(workspaceSlug, projectId, issue.id, user);
handleMutation();
handleClose();
};
useEffect(() => {
if (!peekIssue) return;
getIssueById(workspaceSlug, projectId, peekIssue.toString());
}, [getIssueById, peekIssue, projectId, workspaceSlug]);
useEffect(() => {
if (peekIssue) {
if (peekOverviewMode === "side") {
setIsSidePeekOpen(true);
setIsModalPeekOpen(false);
} else {
setIsModalPeekOpen(true);
setIsSidePeekOpen(false);
}
} else {
console.log("Triggered");
setIsSidePeekOpen(false);
setIsModalPeekOpen(false);
}
}, [peekIssue, peekOverviewMode]);
return (
<>
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="relative h-full w-full">
<Transition.Child
as={React.Fragment}
enter="transition-transform duration-300"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition-transform duration-200"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<Dialog.Panel className="absolute z-20 bg-custom-background-100 top-0 right-0 h-full w-1/2 shadow-custom-shadow-md">
<SidePeekView
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={peekOverviewMode}
readOnly={readOnly}
setMode={(mode) => setPeekOverviewMode(mode)}
workspaceSlug={workspaceSlug}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
<Transition.Root appear show={isModalPeekOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child <Transition.Child
as={React.Fragment} as={React.Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enterFrom="opacity-0"
enterTo="opacity-100 translate-y-0 sm:scale-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0"
> >
<Dialog.Panel <div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
className={`absolute z-20 bg-custom-background-100 ${
peekOverviewMode === "side"
? "top-0 right-0 h-full w-1/2 shadow-custom-shadow-md"
: peekOverviewMode === "modal"
? "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[70%] w-3/5 rounded-lg shadow-custom-shadow-xl"
: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[95%] w-[95%] rounded-lg shadow-custom-shadow-xl"
}`}
>
{(peekOverviewMode === "side" || peekOverviewMode === "modal") && (
<SidePeekView
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={peekOverviewMode}
readOnly={readOnly}
setMode={(mode) => setPeekOverviewMode(mode)}
workspaceSlug={workspaceSlug}
/>
)}
{peekOverviewMode === "full" && (
<FullScreenPeekView
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={peekOverviewMode}
readOnly={readOnly}
setMode={(mode) => setPeekOverviewMode(mode)}
workspaceSlug={workspaceSlug}
/>
)}
</Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> <div className="fixed inset-0 z-20 overflow-y-auto">
</div> <div className="relative h-full w-full">
</Dialog> <Transition.Child
</Transition.Root> as={React.Fragment}
); enter="ease-out duration-300"
}; enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Panel
className={`absolute z-20 bg-custom-background-100 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg shadow-custom-shadow-xl transition-all duration-300 ${
peekOverviewMode === "modal" ? "h-[70%] w-3/5" : "h-[95%] w-[95%]"
}`}
>
{peekOverviewMode === "modal" && (
<SidePeekView
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={peekOverviewMode}
readOnly={readOnly}
setMode={(mode) => setPeekOverviewMode(mode)}
workspaceSlug={workspaceSlug}
/>
)}
{peekOverviewMode === "full" && (
<FullScreenPeekView
handleClose={handleClose}
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={peekOverviewMode}
readOnly={readOnly}
setMode={(mode) => setPeekOverviewMode(mode)}
workspaceSlug={workspaceSlug}
/>
)}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
);
}
);

View File

@ -1,3 +1,4 @@
// components
import { import {
PeekOverviewHeader, PeekOverviewHeader,
PeekOverviewIssueActivity, PeekOverviewIssueActivity,
@ -5,13 +6,16 @@ import {
PeekOverviewIssueProperties, PeekOverviewIssueProperties,
TPeekOverviewModes, TPeekOverviewModes,
} from "components/issues"; } from "components/issues";
// ui
import { Loader } from "components/ui";
// types
import { IIssue } from "types"; import { IIssue } from "types";
type Props = { type Props = {
handleClose: () => void; handleClose: () => void;
handleDeleteIssue: () => void; handleDeleteIssue: () => void;
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>; handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
issue: IIssue; issue: IIssue | undefined;
mode: TPeekOverviewModes; mode: TPeekOverviewModes;
readOnly: boolean; readOnly: boolean;
setMode: (mode: TPeekOverviewModes) => void; setMode: (mode: TPeekOverviewModes) => void;
@ -39,37 +43,50 @@ export const SidePeekView: React.FC<Props> = ({
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
/> />
</div> </div>
<div className="h-full w-full px-6 overflow-y-auto"> {issue ? (
{/* issue title and description */} <div className="h-full w-full px-6 overflow-y-auto">
<div className="w-full"> {/* issue title and description */}
<PeekOverviewIssueDetails <div className="w-full">
handleUpdateIssue={handleUpdateIssue} <PeekOverviewIssueDetails
issue={issue} handleUpdateIssue={handleUpdateIssue}
readOnly={readOnly} issue={issue}
workspaceSlug={workspaceSlug} readOnly={readOnly}
/> workspaceSlug={workspaceSlug}
/>
</div>
{/* issue properties */}
<div className="w-full mt-10">
<PeekOverviewIssueProperties
handleDeleteIssue={handleDeleteIssue}
handleUpdateIssue={handleUpdateIssue}
issue={issue}
mode={mode}
readOnly={readOnly}
workspaceSlug={workspaceSlug}
/>
</div>
{/* divider */}
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
{/* issue activity/comments */}
<div className="w-full pb-5">
{issue && (
<PeekOverviewIssueActivity
workspaceSlug={workspaceSlug}
issue={issue}
readOnly={readOnly}
/>
)}
</div>
</div> </div>
{/* issue properties */} ) : (
<div className="w-full mt-10"> <Loader className="px-6">
<PeekOverviewIssueProperties <Loader.Item height="30px" />
handleDeleteIssue={handleDeleteIssue} <div className="space-y-2 mt-3">
issue={issue} <Loader.Item height="20px" width="70%" />
mode={mode} <Loader.Item height="20px" width="60%" />
onChange={handleUpdateIssue} <Loader.Item height="20px" width="60%" />
readOnly={readOnly} </div>
workspaceSlug={workspaceSlug} </Loader>
/> )}
</div>
{/* divider */}
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
{/* issue activity/comments */}
<div className="w-full pb-5">
<PeekOverviewIssueActivity
workspaceSlug={workspaceSlug}
issue={issue}
readOnly={readOnly}
/>
</div>
</div>
</div> </div>
); );

View File

@ -48,7 +48,7 @@ const useSpreadsheetIssuesView = () => {
sub_issue: "false", sub_issue: "false",
}; };
const { data: projectSpreadsheetIssues } = useSWR( const { data: projectSpreadsheetIssues, mutate: mutateProjectSpreadsheetIssues } = useSWR(
workspaceSlug && projectId workspaceSlug && projectId
? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params) ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params)
: null, : null,
@ -58,7 +58,7 @@ const useSpreadsheetIssuesView = () => {
: null : null
); );
const { data: cycleSpreadsheetIssues } = useSWR( const { data: cycleSpreadsheetIssues, mutate: mutateCycleSpreadsheetIssues } = useSWR(
workspaceSlug && projectId && cycleId workspaceSlug && projectId && cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
: null, : null,
@ -73,7 +73,7 @@ const useSpreadsheetIssuesView = () => {
: null : null
); );
const { data: moduleSpreadsheetIssues } = useSWR( const { data: moduleSpreadsheetIssues, mutate: mutateModuleSpreadsheetIssues } = useSWR(
workspaceSlug && projectId && moduleId workspaceSlug && projectId && moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: null, : null,
@ -88,7 +88,7 @@ const useSpreadsheetIssuesView = () => {
: null : null
); );
const { data: viewSpreadsheetIssues } = useSWR( const { data: viewSpreadsheetIssues, mutate: mutateViewSpreadsheetIssues } = useSWR(
workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null, workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null,
workspaceSlug && projectId && viewId && params workspaceSlug && projectId && viewId && params
? () => ? () =>
@ -106,6 +106,13 @@ const useSpreadsheetIssuesView = () => {
return { return {
issueView, issueView,
mutateIssues: cycleId
? mutateCycleSpreadsheetIssues
: moduleId
? mutateModuleSpreadsheetIssues
: viewId
? mutateViewSpreadsheetIssues
: mutateProjectSpreadsheetIssues,
spreadsheetIssues: spreadsheetIssues ?? [], spreadsheetIssues: spreadsheetIssues ?? [],
orderBy, orderBy,
setOrderBy, setOrderBy,

172
apps/app/store/issues.ts Normal file
View File

@ -0,0 +1,172 @@
// mobx
import { action, observable, runInAction, makeAutoObservable } from "mobx";
// services
import issueService from "services/issues.service";
// types
import type { ICurrentUserResponse, IIssue } from "types";
class IssuesStore {
issues: { [key: string]: IIssue } = {};
isIssuesLoading: boolean = false;
rootStore: any | null = null;
constructor(_rootStore: any | null = null) {
makeAutoObservable(this, {
issues: observable.ref,
loadIssues: action,
getIssueById: action,
isIssuesLoading: observable,
createIssue: action,
updateIssue: action,
deleteIssue: action,
});
this.rootStore = _rootStore;
}
/**
* @description Fetch all issues of a project and hydrate issues field
*/
loadIssues = async (workspaceSlug: string, projectId: string) => {
this.isIssuesLoading = true;
try {
const issuesResponse: IIssue[] = (await issueService.getIssuesWithParams(
workspaceSlug,
projectId
)) as IIssue[];
const issues: { [kye: string]: IIssue } = {};
issuesResponse.forEach((issue) => {
issues[issue.id] = issue;
});
runInAction(() => {
this.issues = issues;
this.isIssuesLoading = false;
});
} catch (error) {
this.isIssuesLoading = false;
console.error("Fetching issues error", error);
}
};
getIssueById = async (
workspaceSlug: string,
projectId: string,
issueId: string
): Promise<IIssue> => {
if (this.issues[issueId]) return this.issues[issueId];
try {
const issueResponse: IIssue = await issueService.retrieve(workspaceSlug, projectId, issueId);
const issues = {
...this.issues,
[issueId]: { ...issueResponse },
};
runInAction(() => {
this.issues = issues;
});
return issueResponse;
} catch (error) {
throw error;
}
};
createIssue = async (
workspaceSlug: string,
projectId: string,
issueForm: IIssue,
user: ICurrentUserResponse
): Promise<IIssue> => {
try {
const issueResponse = await issueService.createIssues(
workspaceSlug,
projectId,
issueForm,
user
);
const issues = {
...this.issues,
[issueResponse.id]: { ...issueResponse },
};
runInAction(() => {
this.issues = issues;
});
return issueResponse;
} catch (error) {
console.error("Creating issue error", error);
throw error;
}
};
updateIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
issueForm: Partial<IIssue>,
user: ICurrentUserResponse
) => {
// keep a copy of the issue in the store
const originalIssue = { ...this.issues[issueId] };
// immediately update the issue in the store
const updatedIssue = { ...originalIssue, ...issueForm };
try {
runInAction(() => {
this.issues[issueId] = updatedIssue;
});
// make a patch request to update the issue
const issueResponse: IIssue = await issueService.patchIssue(
workspaceSlug,
projectId,
issueId,
issueForm,
user
);
const updatedIssues = { ...this.issues };
updatedIssues[issueId] = { ...issueResponse };
runInAction(() => {
this.issues = updatedIssues;
});
} catch (error) {
// if there is an error, revert the changes
runInAction(() => {
this.issues[issueId] = originalIssue;
});
return error;
}
};
deleteIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
user: ICurrentUserResponse
) => {
const issues = { ...this.issues };
delete issues[issueId];
try {
runInAction(() => {
this.issues = issues;
});
issueService.deleteIssue(workspaceSlug, projectId, issueId, user);
} catch (error) {
console.error("Deleting issue error", error);
}
};
}
export default IssuesStore;

View File

@ -3,6 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite";
// store imports // store imports
import UserStore from "./user"; import UserStore from "./user";
import ThemeStore from "./theme"; import ThemeStore from "./theme";
import IssuesStore from "./issues";
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish"; import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -11,10 +12,12 @@ export class RootStore {
user; user;
theme; theme;
projectPublish: IProjectPublishStore; projectPublish: IProjectPublishStore;
issues: IssuesStore;
constructor() { constructor() {
this.user = new UserStore(this); this.user = new UserStore(this);
this.theme = new ThemeStore(this); this.theme = new ThemeStore(this);
this.projectPublish = new ProjectPublishStore(this); this.projectPublish = new ProjectPublishStore(this);
this.issues = new IssuesStore(this);
} }
} }