forked from github/plane
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:
parent
17aff1f369
commit
f5a076e9a9
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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)}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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
172
apps/app/store/issues.ts
Normal 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;
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user