[WEB-1792] chore: handled loader state and empty state in notification issue peekoverview (#4985)
* chore: handled loader state and empty state in notification issue peekoverview * chore: code beautyfication
This commit is contained in:
parent
f2694e0be4
commit
c2150687a6
6 changed files with 284 additions and 127 deletions
|
|
@ -36,7 +36,7 @@ const WorkspaceDashboardPage = observer(() => {
|
|||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-ful h-full overflow-hidden overflow-y-auto">
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||
<IssuePeekOverview embedIssue />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
41
web/core/components/issues/peek-overview/error.tsx
Normal file
41
web/core/components/issues/peek-overview/error.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { MoveRight } from "lucide-react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { EmptyState } from "@/components/common";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// images
|
||||
import emptyIssue from "@/public/empty-state/issue.svg";
|
||||
|
||||
type TIssuePeekOverviewError = {
|
||||
removeRoutePeekId: () => void;
|
||||
};
|
||||
|
||||
export const IssuePeekOverviewError: FC<TIssuePeekOverviewError> = (props) => {
|
||||
const { removeRoutePeekId } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden relative flex flex-col">
|
||||
<div className="flex-shrink-0 flex justify-start">
|
||||
<Tooltip tooltipContent="Close the peek view" isMobile={isMobile}>
|
||||
<button onClick={removeRoutePeekId} className="w-5 h-5 m-5">
|
||||
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full">
|
||||
<EmptyState
|
||||
image={emptyIssue ?? undefined}
|
||||
title="Issue does not exist"
|
||||
description="The issue you are looking for does not exist, has been archived, or has been deleted."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,3 +4,6 @@ export * from "./issue-detail";
|
|||
export * from "./properties";
|
||||
export * from "./root";
|
||||
export * from "./view";
|
||||
|
||||
export * from "./loader";
|
||||
export * from "./error";
|
||||
|
|
|
|||
106
web/core/components/issues/peek-overview/loader.tsx
Normal file
106
web/core/components/issues/peek-overview/loader.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { MoveRight } from "lucide-react";
|
||||
import { Loader, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type TIssuePeekOverviewLoader = {
|
||||
removeRoutePeekId: () => void;
|
||||
};
|
||||
|
||||
export const IssuePeekOverviewLoader: FC<TIssuePeekOverviewLoader> = (props) => {
|
||||
const { removeRoutePeekId } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
return (
|
||||
<Loader className="w-full h-full overflow-hidden p-5 space-y-6">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip tooltipContent="Close the peek view" isMobile={isMobile}>
|
||||
<button onClick={removeRoutePeekId}>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="80px" height="30px" />
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* issue title and description and comments */}
|
||||
<div className="space-y-3">
|
||||
<Loader.Item width="100px" height="20px" />
|
||||
|
||||
<div className="space-y-1">
|
||||
<Loader.Item width="300px" height="15px" />
|
||||
<Loader.Item width="400px" height="15px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="20px" height="15px" />
|
||||
<Loader.Item width="500px" height="15px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="20px" height="15px" />
|
||||
<Loader.Item width="200px" height="15px" />
|
||||
</div>
|
||||
<Loader.Item width="300px" height="15px" />
|
||||
<Loader.Item width="200px" height="15px" />
|
||||
</div>
|
||||
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
</div>
|
||||
|
||||
{/* sub issues */}
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<Loader.Item width="80px" height="20px" />
|
||||
<Loader.Item width="100px" height="20px" />
|
||||
</div>
|
||||
|
||||
{/* attachments */}
|
||||
<div className="space-y-3">
|
||||
<Loader.Item width="80px" height="20px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="250px" height="50px" />
|
||||
<Loader.Item width="250px" height="50px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* properties */}
|
||||
<div className="space-y-3">
|
||||
<Loader.Item width="80px" height="20px" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-8">
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
<Loader.Item width="150px" height="25px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,9 +3,7 @@
|
|||
import { FC, useEffect, useState, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { IssueView } from "@/components/issues";
|
||||
|
|
@ -60,7 +58,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
issue: { getIssueById, fetchIssue },
|
||||
issue: { fetchIssue },
|
||||
} = useIssueDetail();
|
||||
const {
|
||||
addCycleToIssue,
|
||||
|
|
@ -72,7 +70,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||
} = useIssueDetail();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
// state
|
||||
const [loader, setLoader] = useState(false);
|
||||
const [loader, setLoader] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const issueOperations: TIssuePeekOperations = useMemo(
|
||||
() => ({
|
||||
|
|
@ -384,27 +383,32 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||
useEffect(() => {
|
||||
if (peekIssue) {
|
||||
setLoader(true);
|
||||
issueOperations.fetch(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => {
|
||||
setLoader(false);
|
||||
});
|
||||
setError(false);
|
||||
issueOperations
|
||||
.fetch(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId)
|
||||
.catch((error) => {
|
||||
console.error("Error fetching the issue", error);
|
||||
setError(true);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoader(false);
|
||||
});
|
||||
}
|
||||
}, [peekIssue, issueOperations]);
|
||||
|
||||
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
||||
|
||||
const issue = getIssueById(peekIssue.issueId) || undefined;
|
||||
|
||||
const currentProjectRole = currentWorkspaceAllProjectsRole?.[peekIssue?.projectId];
|
||||
// Check if issue is editable, based on user role
|
||||
const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
const isLoading = !issue || loader ? true : false;
|
||||
|
||||
return (
|
||||
<IssueView
|
||||
workspaceSlug={peekIssue.workspaceSlug}
|
||||
projectId={peekIssue.projectId}
|
||||
issueId={peekIssue.issueId}
|
||||
isLoading={isLoading}
|
||||
isLoading={loader}
|
||||
isError={error}
|
||||
is_archived={is_archived}
|
||||
disabled={!isEditable}
|
||||
embedIssue={embedIssue}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { FC, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import {
|
||||
DeleteIssueModal,
|
||||
IssuePeekOverviewHeader,
|
||||
|
|
@ -11,6 +9,8 @@ import {
|
|||
TIssueOperations,
|
||||
ArchiveIssueModal,
|
||||
PeekOverviewIssueAttachments,
|
||||
IssuePeekOverviewLoader,
|
||||
IssuePeekOverviewError,
|
||||
} from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
|
@ -27,6 +27,7 @@ interface IIssueView {
|
|||
projectId: string;
|
||||
issueId: string;
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
is_archived: boolean;
|
||||
disabled?: boolean;
|
||||
embedIssue?: boolean;
|
||||
|
|
@ -39,6 +40,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
projectId,
|
||||
issueId,
|
||||
isLoading,
|
||||
isError,
|
||||
is_archived,
|
||||
disabled = false,
|
||||
embedIssue = false,
|
||||
|
|
@ -95,8 +97,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
};
|
||||
|
||||
const peekOverviewIssueClassName = cn(
|
||||
!embedIssue &&
|
||||
"fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300",
|
||||
!embedIssue
|
||||
? "fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300"
|
||||
: `w-full h-full`,
|
||||
!embedIssue && {
|
||||
"bottom-0 right-0 top-0 w-full md:w-[50%]": peekMode === "side-peek",
|
||||
"size-5/6 top-[8.33%] left-[8.33%]": peekMode === "modal",
|
||||
|
|
@ -129,7 +132,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
/>
|
||||
)}
|
||||
|
||||
<div className="w-full !text-base">
|
||||
<div className="w-full h-full !text-base">
|
||||
{issueId && (
|
||||
<div
|
||||
ref={issuePeekOverviewRef}
|
||||
|
|
@ -139,61 +142,122 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
"0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12)",
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<IssuePeekOverviewHeader
|
||||
peekMode={peekMode}
|
||||
setPeekMode={(value) => setPeekMode(value)}
|
||||
removeRoutePeekId={removeRoutePeekId}
|
||||
toggleDeleteIssueModal={toggleDeleteIssueModal}
|
||||
toggleArchiveIssueModal={toggleArchiveIssueModal}
|
||||
handleRestoreIssue={handleRestore}
|
||||
isArchived={is_archived}
|
||||
issueId={issueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isSubmitting={isSubmitting}
|
||||
disabled={disabled}
|
||||
embedIssue={embedIssue}
|
||||
/>
|
||||
{/* content */}
|
||||
<div className="vertical-scrollbar scrollbar-md relative h-full w-full overflow-hidden overflow-y-auto">
|
||||
{isLoading && !issue ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
) : (
|
||||
issue && (
|
||||
<>
|
||||
{["side-peek", "modal"].includes(peekMode) ? (
|
||||
<div className="relative flex flex-col gap-3 px-8 py-5 space-y-3">
|
||||
<PeekOverviewIssueDetails
|
||||
{isLoading && <IssuePeekOverviewLoader removeRoutePeekId={removeRoutePeekId} />}
|
||||
{isError && (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<IssuePeekOverviewError removeRoutePeekId={removeRoutePeekId} />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !isError && issue && (
|
||||
<>
|
||||
{/* header */}
|
||||
<IssuePeekOverviewHeader
|
||||
peekMode={peekMode}
|
||||
setPeekMode={(value) => setPeekMode(value)}
|
||||
removeRoutePeekId={removeRoutePeekId}
|
||||
toggleDeleteIssueModal={toggleDeleteIssueModal}
|
||||
toggleArchiveIssueModal={toggleArchiveIssueModal}
|
||||
handleRestoreIssue={handleRestore}
|
||||
isArchived={is_archived}
|
||||
issueId={issueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isSubmitting={isSubmitting}
|
||||
disabled={disabled}
|
||||
embedIssue={embedIssue}
|
||||
/>
|
||||
{/* content */}
|
||||
<div className="vertical-scrollbar scrollbar-md relative h-full w-full overflow-hidden overflow-y-auto">
|
||||
{["side-peek", "modal"].includes(peekMode) ? (
|
||||
<div className="relative flex flex-col gap-3 px-8 py-5 space-y-3">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived}
|
||||
isArchived={is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
<SubIssuesRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
parentIssueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={disabled || is_archived}
|
||||
isArchived={is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentUser && (
|
||||
<SubIssuesRoot
|
||||
<PeekOverviewIssueAttachments
|
||||
disabled={disabled || is_archived}
|
||||
issueId={issueId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
|
||||
<PeekOverviewProperties
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={is_archived}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="vertical-scrollbar flex h-full w-full overflow-auto">
|
||||
<div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5">
|
||||
<div className="space-y-3">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={issueId}
|
||||
currentUser={currentUser}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived}
|
||||
isArchived={is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PeekOverviewIssueAttachments
|
||||
disabled={disabled || is_archived}
|
||||
issueId={issueId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
{currentUser && (
|
||||
<SubIssuesRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PeekOverviewIssueAttachments
|
||||
disabled={disabled || is_archived}
|
||||
issueId={issueId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={is_archived}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`h-full !w-[400px] flex-shrink-0 border-l border-custom-border-200 p-4 py-5 ${
|
||||
is_archived ? "pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
<PeekOverviewProperties
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
|
|
@ -201,73 +265,12 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={is_archived}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="vertical-scrollbar flex h-full w-full overflow-auto">
|
||||
<div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5">
|
||||
<div className="space-y-3">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived}
|
||||
isArchived={is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
<SubIssuesRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PeekOverviewIssueAttachments
|
||||
disabled={disabled || is_archived}
|
||||
issueId={issueId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={is_archived}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`h-full !w-[400px] flex-shrink-0 border-l border-custom-border-200 p-4 py-5 ${
|
||||
is_archived ? "pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
<PeekOverviewProperties
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue