feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
"use client";
import type { FC } from "react";
import { MoveRight } from "lucide-react";
import { Tooltip } from "@plane/propel/tooltip";
// components
import { EmptyState } from "@/components/common/empty-state";
// 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="Work item does not exist"
description="The work item you are looking for does not exist, has been archived, or has been deleted."
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,254 @@
"use client";
import type { FC } from "react";
import { useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { Link2, MoveDiagonal, MoveRight } from "lucide-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { CenterPanelIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import type { TNameDescriptionLoader } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
import { CustomSelect } from "@plane/ui";
import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues";
import { useProject } from "@/hooks/store/use-project";
import { useUser } from "@/hooks/store/user";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
// local imports
import { IssueSubscription } from "../issue-detail/subscription";
import { WorkItemDetailQuickActions } from "../issue-layouts/quick-action-dropdowns";
import { NameDescriptionUpdateStatus } from "../issue-update-status";
export type TPeekModes = "side-peek" | "modal" | "full-screen";
const PEEK_OPTIONS: { key: TPeekModes; icon: any; i18n_title: string }[] = [
{
key: "side-peek",
icon: SidePanelIcon,
i18n_title: "common.side_peek",
},
{
key: "modal",
icon: CenterPanelIcon,
i18n_title: "common.modal",
},
{
key: "full-screen",
icon: FullScreenPanelIcon,
i18n_title: "common.full_screen",
},
];
export type PeekOverviewHeaderProps = {
peekMode: TPeekModes;
setPeekMode: (value: TPeekModes) => void;
removeRoutePeekId: () => void;
workspaceSlug: string;
projectId: string;
issueId: string;
isArchived: boolean;
disabled: boolean;
embedIssue: boolean;
toggleDeleteIssueModal: (value: boolean) => void;
toggleArchiveIssueModal: (value: boolean) => void;
toggleDuplicateIssueModal: (value: boolean) => void;
toggleEditIssueModal: (value: boolean) => void;
handleRestoreIssue: () => Promise<void>;
isSubmitting: TNameDescriptionLoader;
};
export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((props) => {
const {
peekMode,
setPeekMode,
workspaceSlug,
projectId,
issueId,
isArchived,
disabled,
embedIssue = false,
removeRoutePeekId,
toggleDeleteIssueModal,
toggleArchiveIssueModal,
toggleDuplicateIssueModal,
toggleEditIssueModal,
handleRestoreIssue,
isSubmitting,
} = props;
// ref
const parentRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
// store hooks
const { data: currentUser } = useUser();
const {
issue: { getIssueById },
setPeekIssue,
removeIssue,
archiveIssue,
getIsIssuePeeked,
} = useIssueDetail();
const { isMobile } = usePlatformOS();
const { getProjectIdentifierById } = useProject();
// derived values
const issueDetails = getIssueById(issueId);
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id);
const {
issues: { removeIssue: removeArchivedIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const workItemLink = generateWorkItemLink({
workspaceSlug,
projectId: issueDetails?.project_id,
issueId,
projectIdentifier,
sequenceId: issueDetails?.sequence_id,
isArchived,
});
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
copyUrlToClipboard(workItemLink).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("common.link_copied_to_clipboard"),
});
});
};
const handleDeleteIssue = async () => {
try {
const deleteIssue = issueDetails?.archived_at ? removeArchivedIssue : removeIssue;
return deleteIssue(workspaceSlug, projectId, issueId).then(() => {
setPeekIssue(undefined);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
});
});
} catch (error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
error: error as Error,
});
}
};
const handleArchiveIssue = async () => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
// check and remove if issue is peeked
if (getIsIssuePeeked(issueId)) {
removeRoutePeekId();
}
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
error: error as Error,
});
}
};
return (
<div
className={`relative flex items-center justify-between p-4 ${
currentMode?.key === "full-screen" ? "border-b border-custom-border-200" : ""
}`}
>
<div className="flex items-center gap-4">
<Tooltip tooltipContent={t("common.close_peek_view")} isMobile={isMobile}>
<button onClick={removeRoutePeekId}>
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
<Tooltip tooltipContent={t("issue.open_in_full_screen")} isMobile={isMobile}>
<Link href={workItemLink} onClick={() => removeRoutePeekId()}>
<MoveDiagonal className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</Link>
</Tooltip>
{currentMode && embedIssue === false && (
<div className="flex flex-shrink-0 items-center gap-2">
<CustomSelect
value={currentMode}
onChange={(val: any) => setPeekMode(val)}
customButton={
<Tooltip tooltipContent={t("common.toggle_peek_view_layout")} isMobile={isMobile}>
<button type="button" className="">
<currentMode.icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
}
>
{PEEK_OPTIONS.map((mode) => (
<CustomSelect.Option key={mode.key} value={mode.key}>
<div
className={`flex items-center gap-1.5 ${
currentMode.key === mode.key
? "text-custom-text-200"
: "text-custom-text-400 hover:text-custom-text-200"
}`}
>
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
{t(mode.i18n_title)}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
)}
</div>
<div className="flex items-center gap-x-4">
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
<div className="flex items-center gap-4">
{currentUser && !isArchived && (
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
)}
<Tooltip tooltipContent={t("common.actions.copy_link")} isMobile={isMobile}>
<button type="button" onClick={handleCopyText}>
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
</button>
</Tooltip>
{issueDetails && (
<WorkItemDetailQuickActions
parentRef={parentRef}
issue={issueDetails}
handleDelete={handleDeleteIssue}
handleArchive={handleArchiveIssue}
handleRestore={handleRestoreIssue}
readOnly={disabled}
toggleDeleteIssueModal={toggleDeleteIssueModal}
toggleArchiveIssueModal={toggleArchiveIssueModal}
toggleDuplicateIssueModal={toggleDuplicateIssueModal}
toggleEditIssueModal={toggleEditIssueModal}
isPeekMode
/>
)}
</div>
</div>
</div>
);
});

View File

@@ -0,0 +1 @@
export * from "./root";

View File

@@ -0,0 +1,182 @@
"use-client";
import type { FC } from "react";
import { useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane imports
import type { EditorRefApi } from "@plane/editor";
import type { TNameDescriptionLoader } from "@plane/types";
// components
import { getTextContent } from "@plane/utils";
import { DescriptionVersionsRoot } from "@/components/core/description-versions";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useMember } from "@/hooks/store/use-member";
import { useProject } from "@/hooks/store/use-project";
import { useUser } from "@/hooks/store/user";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// plane web components
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe/duplicate-popover";
import { IssueTypeSwitcher } from "@/plane-web/components/issues/issue-details/issue-type-switcher";
// plane web hooks
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
// services
import { WorkItemVersionService } from "@/services/issue";
// local components
import { IssueDescriptionInput } from "../description-input";
import type { TIssueOperations } from "../issue-detail";
import { IssueParentDetail } from "../issue-detail/parent";
import { IssueReaction } from "../issue-detail/reactions";
import { IssueTitleInput } from "../title-input";
// services init
const workItemVersionService = new WorkItemVersionService();
type Props = {
editorRef: React.RefObject<EditorRefApi>;
workspaceSlug: string;
projectId: string;
issueId: string;
issueOperations: TIssueOperations;
disabled: boolean;
isArchived: boolean;
isSubmitting: TNameDescriptionLoader;
setIsSubmitting: (value: TNameDescriptionLoader) => void;
};
export const PeekOverviewIssueDetails: FC<Props> = observer((props) => {
const { editorRef, workspaceSlug, issueId, issueOperations, disabled, isArchived, isSubmitting, setIsSubmitting } =
props;
// store hooks
const { data: currentUser } = useUser();
const {
issue: { getIssueById },
} = useIssueDetail();
const { getProjectById } = useProject();
const { getUserDetails } = useMember();
// reload confirmation
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
useEffect(() => {
if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => {
setIsSubmitting("saved");
}, 2000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
}
}, [isSubmitting, setShowAlert, setIsSubmitting]);
// derived values
const issue = issueId ? getIssueById(issueId) : undefined;
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
// debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(
workspaceSlug,
projectDetails?.workspace.toString(),
projectDetails?.id,
{
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
if (!issue || !issue.project_id) return <></>;
const issueDescription =
issue.description_html !== undefined || issue.description_html !== null
? issue.description_html != ""
? issue.description_html
: "<p></p>"
: undefined;
return (
<div className="space-y-2">
{issue.parent_id && (
<IssueParentDetail
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
issueId={issueId}
issue={issue}
issueOperations={issueOperations}
/>
)}
<div className="flex items-center justify-between gap-2">
<IssueTypeSwitcher issueId={issueId} disabled={isArchived || disabled} />
{duplicateIssues?.length > 0 && (
<DeDupeIssuePopoverRoot
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
rootIssueId={issueId}
issues={duplicateIssues}
issueOperations={issueOperations}
/>
)}
</div>
<IssueTitleInput
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
issueId={issue.id}
isSubmitting={isSubmitting}
setIsSubmitting={(value) => setIsSubmitting(value)}
issueOperations={issueOperations}
disabled={disabled || isArchived}
value={issue.name}
containerClassName="-ml-3"
/>
<IssueDescriptionInput
editorRef={editorRef}
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
issueId={issue.id}
initialValue={issueDescription}
disabled={disabled || isArchived}
issueOperations={issueOperations}
setIsSubmitting={(value) => setIsSubmitting(value)}
containerClassName="-ml-3 border-none"
/>
<div className="flex items-center justify-between gap-2">
{currentUser && (
<IssueReaction
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
issueId={issueId}
currentUser={currentUser}
disabled={isArchived}
/>
)}
{!disabled && (
<DescriptionVersionsRoot
className="flex-shrink-0"
entityInformation={{
createdAt: issue.created_at ? new Date(issue.created_at) : new Date(),
createdByDisplayName: getUserDetails(issue.created_by ?? "")?.display_name ?? "",
id: issueId,
isRestoreDisabled: disabled || isArchived,
}}
fetchHandlers={{
listDescriptionVersions: (issueId) =>
workItemVersionService.listDescriptionVersions(
workspaceSlug,
issue.project_id?.toString() ?? "",
issueId
),
retrieveDescriptionVersion: (issueId, versionId) =>
workItemVersionService.retrieveDescriptionVersion(
workspaceSlug,
issue.project_id?.toString() ?? "",
issueId,
versionId
),
}}
handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)}
projectId={issue.project_id}
workspaceSlug={workspaceSlug}
/>
)}
</div>
</div>
);
});

View File

@@ -0,0 +1,107 @@
"use client";
import type { FC } from "react";
import { MoveRight } from "lucide-react";
import { Tooltip } from "@plane/propel/tooltip";
import { Loader } 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-screen 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>
);
};

View File

@@ -0,0 +1,308 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, Users, UserCircle2 } from "lucide-react";
// i18n
import { useTranslation } from "@plane/i18n";
// ui icons
import { CycleIcon, DoubleCircleIcon, ModuleIcon } from "@plane/propel/icons";
import { cn, getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils";
// components
import { DateDropdown } from "@/components/dropdowns/date";
import { EstimateDropdown } from "@/components/dropdowns/estimate";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
import { PriorityDropdown } from "@/components/dropdowns/priority";
import { StateDropdown } from "@/components/dropdowns/state/dropdown";
// helpers
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useMember } from "@/hooks/store/use-member";
import { useProject } from "@/hooks/store/use-project";
import { useProjectState } from "@/hooks/store/use-project-state";
// plane web components
import { WorkItemAdditionalSidebarProperties } from "@/plane-web/components/issues/issue-details/additional-properties";
import { IssueParentSelectRoot } from "@/plane-web/components/issues/issue-details/parent-select-root";
import { IssueWorklogProperty } from "@/plane-web/components/issues/worklog/property";
import type { TIssueOperations } from "../issue-detail";
import { IssueCycleSelect } from "../issue-detail/cycle-select";
import { IssueLabel } from "../issue-detail/label";
import { IssueModuleSelect } from "../issue-detail/module-select";
interface IPeekOverviewProperties {
workspaceSlug: string;
projectId: string;
issueId: string;
disabled: boolean;
issueOperations: TIssueOperations;
}
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, disabled } = props;
const { t } = useTranslation();
// store hooks
const { getProjectById } = useProject();
const {
issue: { getIssueById },
} = useIssueDetail();
const { getStateById } = useProjectState();
const { getUserDetails } = useMember();
// derived values
const issue = getIssueById(issueId);
if (!issue) return <></>;
const createdByDetails = getUserDetails(issue?.created_by);
const projectDetails = getProjectById(issue.project_id);
const isEstimateEnabled = projectDetails?.estimate;
const stateDetails = getStateById(issue.state_id);
const minDate = getDate(issue.start_date);
minDate?.setDate(minDate.getDate());
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
return (
<div>
<h6 className="text-sm font-medium">{t("common.properties")}</h6>
{/* TODO: render properties using a common component */}
<div className={`w-full space-y-2 mt-3 ${disabled ? "opacity-60" : ""}`}>
{/* state */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
<span>{t("common.state")}</span>
</div>
<StateDropdown
value={issue?.state_id}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
projectId={projectId}
disabled={disabled}
buttonVariant="transparent-with-text"
className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName="text-sm"
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/>
</div>
{/* assignee */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<Users className="h-4 w-4 flex-shrink-0" />
<span>{t("common.assignees")}</span>
</div>
<MemberDropdown
value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={disabled}
projectId={projectId}
placeholder={t("issue.add.assignee")}
multiple
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${issue?.assignee_ids?.length > 0 ? "" : "text-custom-text-400"}`}
hideIcon={issue.assignee_ids?.length === 0}
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/>
</div>
{/* priority */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<Signal className="h-4 w-4 flex-shrink-0" />
<span>{t("common.priority")}</span>
</div>
<PriorityDropdown
value={issue?.priority}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
disabled={disabled}
buttonVariant="border-with-text"
className="w-3/4 flex-grow rounded px-2 hover:bg-custom-background-80 group"
buttonContainerClassName="w-full text-left"
buttonClassName="w-min h-auto whitespace-nowrap"
/>
</div>
{/* created by */}
{createdByDetails && (
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
<span>{t("common.created_by")}</span>
</div>
<div className="w-full h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-not-allowed">
<ButtonAvatars
showTooltip
userIds={createdByDetails?.display_name.includes("-intake") ? null : createdByDetails?.id}
/>
<span className="flex-grow truncate leading-5">
{createdByDetails?.display_name.includes("-intake") ? "Plane" : createdByDetails?.display_name}
</span>
</div>
</div>
)}
{/* start date */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CalendarClock className="h-4 w-4 flex-shrink-0" />
<span>{t("common.order_by.start_date")}</span>
</div>
<DateDropdown
value={issue.start_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
start_date: val ? renderFormattedPayloadDate(val) : null,
})
}
placeholder={t("issue.add.start_date")}
buttonVariant="transparent-with-text"
maxDate={maxDate ?? undefined}
disabled={disabled}
className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline"
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
{/* due date */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
<span>{t("common.order_by.due_date")}</span>
</div>
<DateDropdown
value={issue.target_date}
onChange={(val) =>
issueOperations.update(workspaceSlug, projectId, issueId, {
target_date: val ? renderFormattedPayloadDate(val) : null,
})
}
placeholder={t("issue.add.due_date")}
buttonVariant="transparent-with-text"
minDate={minDate ?? undefined}
disabled={disabled}
className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={cn("text-sm", {
"text-custom-text-400": !issue.target_date,
"text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group),
})}
hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100"
// TODO: add this logic
// showPlaceholderIcon
/>
</div>
{/* estimate */}
{isEstimateEnabled && (
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<Triangle className="h-4 w-4 flex-shrink-0" />
<span>{t("common.estimate")}</span>
</div>
<EstimateDropdown
value={issue.estimate_point ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })}
projectId={projectId}
disabled={disabled}
buttonVariant="transparent-with-text"
className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.estimate_point !== undefined ? "" : "text-custom-text-400"}`}
placeholder="None"
hideIcon
dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
/>
</div>
)}
{projectDetails?.module_view && (
<div className="flex w-full items-center gap-3 min-h-8 h-full">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<ModuleIcon className="h-4 w-4 flex-shrink-0" />
<span>{t("common.modules")}</span>
</div>
<IssueModuleSelect
className="w-3/4 flex-grow"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
disabled={disabled}
/>
</div>
)}
{projectDetails?.cycle_view && (
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CycleIcon className="h-4 w-4 flex-shrink-0" />
<span>{t("common.cycle")}</span>
</div>
<IssueCycleSelect
className="w-3/4 flex-grow"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
disabled={disabled}
/>
</div>
)}
{/* parent */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<LayoutPanelTop className="h-4 w-4 flex-shrink-0" />
<p>{t("common.parent")}</p>
</div>
<IssueParentSelectRoot
className="w-3/4 flex-grow h-full"
disabled={disabled}
issueId={issueId}
issueOperations={issueOperations}
projectId={projectId}
workspaceSlug={workspaceSlug}
/>
</div>
{/* label */}
<div className="flex w-full items-center gap-3 min-h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<Tag className="h-4 w-4 flex-shrink-0" />
<span>{t("common.labels")}</span>
</div>
<div className="flex w-full flex-col gap-3 truncate">
<IssueLabel workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={disabled} />
</div>
</div>
<IssueWorklogProperty
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
/>
<WorkItemAdditionalSidebarProperties
workItemId={issue.id}
workItemTypeId={issue.type_id}
projectId={projectId}
workspaceSlug={workspaceSlug}
isEditable={!disabled}
isPeekView
/>
</div>
</div>
);
});

View File

@@ -0,0 +1,316 @@
"use client";
import type { FC } from "react";
import { useEffect, useState, useMemo, useCallback } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
// Plane imports
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/propel/toast";
import type { IWorkItemPeekOverview, TIssue } from "@plane/types";
import { EIssueServiceType, EIssuesStoreType } from "@plane/types";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues";
import { useUserPermissions } from "@/hooks/store/user";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties";
// local imports
import type { TIssueOperations } from "../issue-detail";
import { IssueView } from "./view";
export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) => {
const {
embedIssue = false,
embedRemoveCurrentNotification,
is_draft = false,
storeType: issueStoreFromProps,
} = props;
const { t } = useTranslation();
// router
const pathname = usePathname();
// store hook
const { allowPermissions } = useUserPermissions();
const {
issues: { restoreIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const {
peekIssue,
setPeekIssue,
issue: { fetchIssue, getIsFetchingIssueDetails },
fetchActivities,
} = useIssueDetail();
const issueStoreType = useIssueStoreType();
const storeType = issueStoreFromProps ?? issueStoreType;
const { issues } = useIssues(storeType);
useWorkItemProperties(
peekIssue?.projectId,
peekIssue?.workspaceSlug,
peekIssue?.issueId,
storeType === EIssuesStoreType.EPIC ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES
);
// state
const [error, setError] = useState(false);
const removeRoutePeekId = useCallback(() => {
setPeekIssue(undefined);
if (embedIssue) embedRemoveCurrentNotification?.();
}, [embedIssue, embedRemoveCurrentNotification, setPeekIssue]);
const issueOperations: TIssueOperations = useMemo(
() => ({
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
setError(false);
await fetchIssue(workspaceSlug, projectId, issueId);
} catch (error) {
setError(true);
console.error("Error fetching the parent issue", error);
}
},
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
if (issues?.updateIssue) {
await issues
.updateIssue(workspaceSlug, projectId, issueId, data)
.then(async () => {
fetchActivities(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
})
.catch((error) => {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("entity.update.failed", { entity: t("issue.label", { count: 1 }) }),
});
});
}
},
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
});
removeRoutePeekId();
});
} catch (error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId },
error: error as Error,
});
}
},
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
if (!issues?.archiveIssue) return;
await issues.archiveIssue(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId },
error: error as Error,
});
}
},
restore: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await restoreIssue(workspaceSlug, projectId, issueId);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("issue.restore.success.title"),
message: t("issue.restore.success.message"),
});
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.restore,
payload: { id: issueId },
});
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("issue.restore.failed.message"),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.restore,
payload: { id: issueId },
error: error as Error,
});
}
},
addCycleToIssue: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
try {
await issues.addCycleToIssue(workspaceSlug, projectId, cycleId, issueId);
fetchActivities(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("issue.add.cycle.failed"),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
}
},
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
try {
await issues.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueIds },
});
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("issue.add.cycle.failed"),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueIds },
error: error as Error,
});
}
},
removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
try {
const removeFromCyclePromise = issues.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
setPromiseToast(removeFromCyclePromise, {
loading: t("issue.remove.cycle.loading"),
success: {
title: t("toast.success"),
message: () => t("issue.remove.cycle.success"),
},
error: {
title: t("toast.error"),
message: () => t("issue.remove.cycle.failed"),
},
});
await removeFromCyclePromise;
fetchActivities(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
}
},
changeModulesInIssue: async (
workspaceSlug: string,
projectId: string,
issueId: string,
addModuleIds: string[],
removeModuleIds: string[]
) => {
const promise = await issues.changeModulesInIssue(
workspaceSlug,
projectId,
issueId,
addModuleIds,
removeModuleIds
);
fetchActivities(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
return promise;
},
removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
try {
const removeFromModulePromise = issues.removeIssuesFromModule(workspaceSlug, projectId, moduleId, [issueId]);
setPromiseToast(removeFromModulePromise, {
loading: t("issue.remove.module.loading"),
success: {
title: t("toast.success"),
message: () => t("issue.remove.module.success"),
},
error: {
title: t("toast.error"),
message: () => t("issue.remove.module.failed"),
},
});
await removeFromModulePromise;
fetchActivities(workspaceSlug, projectId, issueId);
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
});
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId },
error: error as Error,
});
}
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[fetchIssue, is_draft, issues, fetchActivities, pathname, removeRoutePeekId, restoreIssue]
);
useEffect(() => {
if (peekIssue) {
issueOperations.fetch(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
}
}, [peekIssue, issueOperations]);
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
// Check if issue is editable, based on user role
const isEditable = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
peekIssue?.workspaceSlug,
peekIssue?.projectId
);
return (
<IssueView
workspaceSlug={peekIssue.workspaceSlug}
projectId={peekIssue.projectId}
issueId={peekIssue.issueId}
isLoading={getIsFetchingIssueDetails(peekIssue.issueId)}
isError={error}
is_archived={!!peekIssue.isArchived}
disabled={!isEditable}
embedIssue={embedIssue}
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
issueOperations={issueOperations}
/>
);
});

View File

@@ -0,0 +1,269 @@
import type { FC } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { createPortal } from "react-dom";
// plane imports
import type { EditorRefApi } from "@plane/editor";
import type { TNameDescriptionLoader } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
import { cn } from "@plane/utils";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import useKeypress from "@/hooks/use-keypress";
import usePeekOverviewOutsideClickDetector from "@/hooks/use-peek-overview-outside-click";
// local imports
import type { TIssueOperations } from "../issue-detail";
import { IssueActivity } from "../issue-detail/issue-activity";
import { IssueDetailWidgets } from "../issue-detail-widgets";
import { IssuePeekOverviewError } from "./error";
import type { TPeekModes } from "./header";
import { IssuePeekOverviewHeader } from "./header";
import { PeekOverviewIssueDetails } from "./issue-detail";
import { IssuePeekOverviewLoader } from "./loader";
import { PeekOverviewProperties } from "./properties";
interface IIssueView {
workspaceSlug: string;
projectId: string;
issueId: string;
isLoading?: boolean;
isError?: boolean;
is_archived: boolean;
disabled?: boolean;
embedIssue?: boolean;
embedRemoveCurrentNotification?: () => void;
issueOperations: TIssueOperations;
}
export const IssueView: FC<IIssueView> = observer((props) => {
const {
workspaceSlug,
projectId,
issueId,
isLoading,
isError,
is_archived,
disabled = false,
embedIssue = false,
embedRemoveCurrentNotification,
issueOperations,
} = props;
// states
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
const [isDeleteIssueModalOpen, setIsDeleteIssueModalOpen] = useState(false);
const [isArchiveIssueModalOpen, setIsArchiveIssueModalOpen] = useState(false);
const [isDuplicateIssueModalOpen, setIsDuplicateIssueModalOpen] = useState(false);
const [isEditIssueModalOpen, setIsEditIssueModalOpen] = useState(false);
// ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<EditorRefApi>(null);
// store hooks
const {
setPeekIssue,
isAnyModalOpen,
issue: { getIssueById, getIsLocalDBIssueDescription },
} = useIssueDetail();
const { isAnyModalOpen: isAnyEpicModalOpen } = useIssueDetail(EIssueServiceType.EPICS);
const issue = getIssueById(issueId);
// remove peek id
const removeRoutePeekId = () => {
setPeekIssue(undefined);
if (embedIssue && embedRemoveCurrentNotification) embedRemoveCurrentNotification();
};
const isLocalDBIssueDescription = getIsLocalDBIssueDescription(issueId);
const toggleDeleteIssueModal = (value: boolean) => setIsDeleteIssueModalOpen(value);
const toggleArchiveIssueModal = (value: boolean) => setIsArchiveIssueModalOpen(value);
const toggleDuplicateIssueModal = (value: boolean) => setIsDuplicateIssueModalOpen(value);
const toggleEditIssueModal = (value: boolean) => setIsEditIssueModalOpen(value);
const isAnyLocalModalOpen =
isDeleteIssueModalOpen || isArchiveIssueModalOpen || isDuplicateIssueModalOpen || isEditIssueModalOpen;
usePeekOverviewOutsideClickDetector(
issuePeekOverviewRef,
() => {
const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
if (!embedIssue) {
if (!isAnyModalOpen && !isAnyEpicModalOpen && !isAnyLocalModalOpen && !isAnyDropbarOpen) {
removeRoutePeekId();
}
}
},
issueId
);
const handleKeyDown = () => {
const editorImageFullScreenModalElement = document.querySelector(".editor-image-full-screen-modal");
const dropdownElement = document.activeElement?.tagName === "INPUT";
const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
if (!isAnyModalOpen && !dropdownElement && !isAnyDropbarOpen && !editorImageFullScreenModalElement) {
removeRoutePeekId();
const issueElement = document.getElementById(`issue-${issueId}`);
if (issueElement) issueElement?.focus();
}
};
useKeypress("Escape", () => !embedIssue && handleKeyDown());
const handleRestore = async () => {
if (!issueOperations.restore) return;
await issueOperations.restore(workspaceSlug, projectId, issueId);
removeRoutePeekId();
};
const peekOverviewIssueClassName = cn(
!embedIssue
? "absolute z-[25] flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300"
: `w-full h-full`,
!embedIssue && {
"top-0 bottom-0 right-0 w-full md:w-[50%] border-0 border-l": peekMode === "side-peek",
"size-5/6 top-[8.33%] left-[8.33%]": peekMode === "modal",
"inset-0 m-4 absolute": peekMode === "full-screen",
}
);
const shouldUsePortal = !embedIssue;
const portalContainer = document.getElementById("full-screen-portal") as HTMLElement;
const content = (
<div className="w-full !text-base">
{issueId && (
<div
ref={issuePeekOverviewRef}
className={peekOverviewIssueClassName}
style={{
boxShadow:
"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)",
}}
>
{isError ? (
<div className="relative h-screen w-full overflow-hidden">
<IssuePeekOverviewError removeRoutePeekId={removeRoutePeekId} />
</div>
) : (
isLoading && <IssuePeekOverviewLoader removeRoutePeekId={removeRoutePeekId} />
)}
{!isLoading && !isError && issue && (
<>
{/* header */}
<IssuePeekOverviewHeader
peekMode={peekMode}
setPeekMode={(value) => setPeekMode(value)}
removeRoutePeekId={removeRoutePeekId}
toggleDeleteIssueModal={toggleDeleteIssueModal}
toggleArchiveIssueModal={toggleArchiveIssueModal}
toggleDuplicateIssueModal={toggleDuplicateIssueModal}
toggleEditIssueModal={toggleEditIssueModal}
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
editorRef={editorRef}
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
disabled={disabled || isLocalDBIssueDescription}
isArchived={is_archived}
isSubmitting={isSubmitting}
setIsSubmitting={(value) => setIsSubmitting(value)}
/>
<div className="py-2">
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled || is_archived}
issueServiceType={EIssueServiceType.ISSUES}
/>
</div>
<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
editorRef={editorRef}
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
disabled={disabled || isLocalDBIssueDescription}
isArchived={is_archived}
isSubmitting={isSubmitting}
setIsSubmitting={(value) => setIsSubmitting(value)}
/>
<div className="py-2">
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
issueServiceType={EIssueServiceType.ISSUES}
/>
</div>
<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 overflow-hidden vertical-scrollbar scrollbar-sm ${
is_archived ? "pointer-events-none" : ""
}`}
>
<PeekOverviewProperties
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
disabled={disabled || is_archived}
/>
</div>
</div>
)}
</div>
</>
)}
</div>
)}
</div>
);
return <>{shouldUsePortal && portalContainer ? createPortal(content, portalContainer) : content}</>;
});