feat: refine Beam attachments and peek layouts
This commit is contained in:
parent
ff30eeb8fd
commit
81abcb5b36
|
|
@ -213,10 +213,7 @@ export const DateDropdown = observer(function DateDropdown(props: Props) {
|
|||
createPortal(
|
||||
<Combobox.Options data-prevent-outside-click static>
|
||||
<div
|
||||
className={cn(
|
||||
"nodedc-dropdown-surface z-30 my-1 overflow-hidden",
|
||||
optionsClassName
|
||||
)}
|
||||
className={cn("nodedc-dropdown-surface z-[760] my-1 overflow-hidden", optionsClassName)}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import { useCallback, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import type { FileRejection } from "react-dropzone";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UploadCloud } from "lucide-react";
|
||||
import { LayoutGrid, List, UploadCloud } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
import { convertBytesToSize, getFileExtension } from "@plane/utils";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// plane web hooks
|
||||
|
|
@ -25,6 +26,8 @@ import { IssueAttachmentsUploadItem } from "./attachment-list-upload-item";
|
|||
// types
|
||||
import { IssueAttachmentDeleteModal } from "./delete-attachment-modal";
|
||||
|
||||
type TAttachmentViewMode = "grid" | "list";
|
||||
|
||||
type TIssueAttachmentItemList = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
|
|
@ -46,6 +49,10 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
|||
const { t } = useTranslation();
|
||||
// states
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [attachmentViewMode, setAttachmentViewMode] = useState<TAttachmentViewMode>(() => {
|
||||
if (typeof window === "undefined") return "grid";
|
||||
return window.localStorage.getItem("nodedc-issue-attachment-view-mode") === "list" ? "list" : "grid";
|
||||
});
|
||||
// store hooks
|
||||
const {
|
||||
attachment: { getAttachmentsByIssueId },
|
||||
|
|
@ -67,6 +74,7 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
|||
activeUploads.length > 0
|
||||
? Math.round(activeUploads.reduce((progressSum, item) => progressSum + item.progress, 0) / activeUploads.length)
|
||||
: 0;
|
||||
const isListView = attachmentViewMode === "list";
|
||||
|
||||
// handlers
|
||||
const handleFetchPropertyActivities = useCallback(() => {
|
||||
|
|
@ -122,6 +130,14 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
|||
disabled: isUploading || disabled,
|
||||
});
|
||||
|
||||
const handleToggleAttachmentViewMode = useCallback(() => {
|
||||
setAttachmentViewMode((currentMode) => {
|
||||
const nextMode: TAttachmentViewMode = currentMode === "grid" ? "list" : "grid";
|
||||
if (typeof window !== "undefined") window.localStorage.setItem("nodedc-issue-attachment-view-mode", nextMode);
|
||||
return nextMode;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{attachmentDeleteModalId && (
|
||||
|
|
@ -173,13 +189,73 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
|||
)}
|
||||
|
||||
{hasAttachmentRows && (
|
||||
<div className="rounded-[1.35rem] border border-subtle bg-surface-1/60 p-3 shadow-[0_12px_28px_rgba(0,0,0,0.08)]">
|
||||
<div
|
||||
className="nodedc-attachments-panel rounded-[1.35rem] border border-subtle bg-surface-1/60 p-3 shadow-[0_12px_28px_rgba(0,0,0,0.08)]"
|
||||
data-view-mode={attachmentViewMode}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3 px-1">
|
||||
<div className="text-12 font-semibold tracking-[0.08em] text-tertiary uppercase">Вложения</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-12 text-tertiary">
|
||||
{activeUploads.length > 0 ? `Загрузка ${uploadProgress}%` : issueAttachments.length}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-8 place-items-center rounded-full bg-surface-2/80 text-tertiary transition hover:bg-surface-2 hover:text-primary"
|
||||
onClick={handleToggleAttachmentViewMode}
|
||||
title={isListView ? "Показать плитками" : "Показать списком"}
|
||||
>
|
||||
{isListView ? <LayoutGrid className="size-4" /> : <List className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isListView ? (
|
||||
<div className="nodedc-attachments-list-scroll overflow-auto rounded-2xl bg-surface-2/40">
|
||||
<div className="min-w-[760px]">
|
||||
<div className="grid grid-cols-[minmax(240px,1.45fr)_120px_86px_minmax(130px,0.9fr)_112px] gap-3 border-b border-subtle px-4 py-2 text-11 font-semibold tracking-[0.05em] text-tertiary uppercase">
|
||||
<div>Файл</div>
|
||||
<div>Дата</div>
|
||||
<div>Версия</div>
|
||||
<div>Добавил</div>
|
||||
<div className="text-right">Действия</div>
|
||||
</div>
|
||||
{uploadStatus?.map((status) => (
|
||||
<div
|
||||
key={status.id}
|
||||
className="grid grid-cols-[minmax(240px,1.45fr)_120px_86px_minmax(130px,0.9fr)_112px] items-center gap-3 border-b border-subtle/70 px-4 py-3 text-12"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-semibold text-secondary">{status.name}</div>
|
||||
<div className="mt-1 text-11 text-tertiary">
|
||||
{(getFileExtension(status.name) || "file").toUpperCase()} · {convertBytesToSize(status.size)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-tertiary">Загрузка</div>
|
||||
<div className="text-secondary">v1</div>
|
||||
<div className="text-tertiary">{status.progress}%</div>
|
||||
<div className="h-1 overflow-hidden rounded-full bg-white/8">
|
||||
<div
|
||||
className="h-full rounded-full bg-[rgb(var(--nodedc-accent-rgb))]"
|
||||
style={{ width: `${status.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{issueAttachments.map((attachmentId) => (
|
||||
<IssueAttachmentsListItem
|
||||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
issueId={issueId}
|
||||
issueServiceType={issueServiceType}
|
||||
projectId={projectId}
|
||||
viewMode="list"
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-3 overflow-x-auto pb-1">
|
||||
{uploadStatus?.map((status) => (
|
||||
<IssueAttachmentsUploadItem key={status.id} uploadStatus={status} />
|
||||
|
|
@ -196,6 +272,7 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||
|
|
@ -45,6 +46,7 @@ type TIssueAttachmentsListItem = {
|
|||
issueServiceType?: TIssueServiceType;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
viewMode?: "grid" | "list";
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
|
|
@ -144,6 +146,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|||
issueId,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
projectId,
|
||||
viewMode = "grid",
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
// store hooks
|
||||
|
|
@ -199,6 +202,17 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|||
"Оригинальная модель загружена в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов.";
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const [isThumbnailError, setIsThumbnailError] = useState(false);
|
||||
const rawVersion = attachment?.attributes.version;
|
||||
const versionLabel =
|
||||
typeof rawVersion === "number"
|
||||
? `v${rawVersion}`
|
||||
: typeof rawVersion === "string"
|
||||
? rawVersion.toLowerCase().startsWith("v")
|
||||
? rawVersion
|
||||
: `v${rawVersion}`
|
||||
: "v1";
|
||||
const uploadedAt = renderFormattedDate(attachment?.created_at ?? attachment?.updated_at ?? "");
|
||||
const uploadedBy = attachment?.created_by ? (getUserDetails(attachment.created_by)?.display_name ?? "—") : "—";
|
||||
const openModelViewer = () => {
|
||||
if (!modelViewerUrl) return;
|
||||
dispatchBeamViewerOpenEvent({
|
||||
|
|
@ -294,6 +308,148 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|||
|
||||
if (!attachment) return <></>;
|
||||
|
||||
if (viewMode === "list") {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-[minmax(240px,1.45fr)_120px_86px_minmax(130px,0.9fr)_112px] items-center gap-3 border-b border-subtle/70 px-4 py-3 text-12 transition hover:bg-white/[0.035]">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 items-center gap-3 text-left"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (modelViewerUrl) openModelViewer();
|
||||
else setIsPreviewOpen(true);
|
||||
}}
|
||||
>
|
||||
<div className="grid size-9 flex-shrink-0 place-items-center rounded-xl bg-surface-1 text-tertiary">
|
||||
{isBeamModel ? (
|
||||
isBeamConversionFailed ? (
|
||||
<AlertTriangle className="text-red-400 size-5" />
|
||||
) : isBeamAwaitingPreview ? (
|
||||
<Spinner
|
||||
height="20px"
|
||||
width="20px"
|
||||
className="fill-[rgb(var(--nodedc-accent-rgb))] text-[rgba(var(--nodedc-accent-rgb),0.18)]"
|
||||
/>
|
||||
) : (
|
||||
<Box className="size-5 text-[rgb(var(--nodedc-accent-rgb))]" />
|
||||
)
|
||||
) : previewType === "file" ? (
|
||||
getFileIcon(fileExtension, 18)
|
||||
) : (
|
||||
<ImageIcon className="size-5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<Tooltip tooltipContent={fullFileName} isMobile={isMobile}>
|
||||
<div className="truncate font-semibold text-secondary">{fullFileName}</div>
|
||||
</Tooltip>
|
||||
<div className="mt-1 flex min-w-0 items-center gap-2 text-11 text-tertiary">
|
||||
<span>{fileExtension ? fileExtension.toUpperCase() : "FILE"}</span>
|
||||
<span className="size-1 rounded-full bg-layer-1" />
|
||||
<span>{convertBytesToSize(attachment.attributes.size)}</span>
|
||||
{beamConversionStatusLabel && (
|
||||
<>
|
||||
<span className="size-1 rounded-full bg-layer-1" />
|
||||
<Tooltip tooltipContent={beamStatusTooltipContent} isMobile={isMobile}>
|
||||
<span
|
||||
className={
|
||||
isBeamConversionFailed
|
||||
? "text-red-400"
|
||||
: isBeamAwaitingPreview
|
||||
? "text-[rgb(var(--nodedc-accent-rgb))]"
|
||||
: "text-tertiary"
|
||||
}
|
||||
>
|
||||
{beamConversionStatusLabel}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="truncate text-tertiary">{uploadedAt}</div>
|
||||
<div className="text-secondary">{versionLabel}</div>
|
||||
<div className="truncate text-tertiary">{uploadedBy}</div>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{(modelViewerUrl || previewURL) && (
|
||||
<Tooltip
|
||||
tooltipContent={modelViewerUrl ? "Посмотреть модель" : "Открыть предпросмотр"}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-8 place-items-center rounded-full text-tertiary transition hover:bg-surface-1 hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (modelViewerUrl) openModelViewer();
|
||||
else setIsPreviewOpen(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{previewDownloadUrl && (
|
||||
<Tooltip tooltipContent={modelDownloadUrl ? "Скачать оригинал" : "Скачать файл"} isMobile={isMobile}>
|
||||
<a
|
||||
href={previewDownloadUrl}
|
||||
download={fullFileName}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="grid size-8 place-items-center rounded-full text-tertiary transition hover:bg-surface-1 hover:text-primary"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Download className="size-4" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip tooltipContent={t("common.actions.delete")} isMobile={isMobile}>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-red-500/10 hover:text-red-400 grid size-8 place-items-center rounded-full text-tertiary transition disabled:pointer-events-none disabled:opacity-40"
|
||||
disabled={!!disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDeleteAttachmentModal(attachmentId);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalCore
|
||||
isOpen={isPreviewOpen}
|
||||
handleClose={() => setIsPreviewOpen(false)}
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.VIIXL}
|
||||
className="h-[calc(100vh-4rem)] max-w-[calc(100vw-4rem)] overflow-hidden border border-subtle bg-surface-1"
|
||||
>
|
||||
<AttachmentPreviewContent
|
||||
beamConversionStatusLabel={beamConversionStatusLabel}
|
||||
beamStatusTooltipContent={beamStatusTooltipContent}
|
||||
fileIcon={fileIcon}
|
||||
fullFileName={fullFileName}
|
||||
isBeamAwaitingPreview={isBeamAwaitingPreview}
|
||||
isBeamConversionFailed={isBeamConversionFailed}
|
||||
modelDownloadUrl={modelDownloadUrl}
|
||||
previewDownloadUrl={previewDownloadUrl}
|
||||
previewType={previewType}
|
||||
previewURL={previewURL}
|
||||
setIsPreviewOpen={setIsPreviewOpen}
|
||||
size={attachment.attributes.size}
|
||||
/>
|
||||
</ModalCore>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="group flex h-32 w-72 flex-shrink-0 overflow-hidden rounded-2xl border border-subtle bg-surface-2/80 transition hover:border-[rgba(var(--nodedc-accent-rgb),0.45)] hover:bg-surface-2">
|
||||
|
|
@ -400,6 +556,57 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|||
width={EModalWidth.VIIXL}
|
||||
className="h-[calc(100vh-4rem)] max-w-[calc(100vw-4rem)] overflow-hidden border border-subtle bg-surface-1"
|
||||
>
|
||||
<AttachmentPreviewContent
|
||||
beamConversionStatusLabel={beamConversionStatusLabel}
|
||||
beamStatusTooltipContent={beamStatusTooltipContent}
|
||||
fileIcon={fileIcon}
|
||||
fullFileName={fullFileName}
|
||||
isBeamAwaitingPreview={isBeamAwaitingPreview}
|
||||
isBeamConversionFailed={isBeamConversionFailed}
|
||||
modelDownloadUrl={modelDownloadUrl}
|
||||
previewDownloadUrl={previewDownloadUrl}
|
||||
previewType={previewType}
|
||||
previewURL={previewURL}
|
||||
setIsPreviewOpen={setIsPreviewOpen}
|
||||
size={attachment.attributes.size}
|
||||
/>
|
||||
</ModalCore>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
type TAttachmentPreviewContent = {
|
||||
beamConversionStatusLabel: string | null;
|
||||
beamStatusTooltipContent: string;
|
||||
fileIcon: ReactNode;
|
||||
fullFileName: string;
|
||||
isBeamAwaitingPreview: boolean;
|
||||
isBeamConversionFailed: boolean;
|
||||
modelDownloadUrl: string | undefined;
|
||||
previewDownloadUrl: string | undefined;
|
||||
previewType: "image" | "video" | "pdf" | "file";
|
||||
previewURL: string;
|
||||
setIsPreviewOpen: (isOpen: boolean) => void;
|
||||
size: number;
|
||||
};
|
||||
|
||||
const AttachmentPreviewContent = (props: TAttachmentPreviewContent) => {
|
||||
const {
|
||||
beamConversionStatusLabel,
|
||||
beamStatusTooltipContent,
|
||||
fileIcon,
|
||||
fullFileName,
|
||||
isBeamAwaitingPreview,
|
||||
isBeamConversionFailed,
|
||||
modelDownloadUrl,
|
||||
previewDownloadUrl,
|
||||
previewType,
|
||||
previewURL,
|
||||
setIsPreviewOpen,
|
||||
size,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full min-h-0 flex-col bg-surface-1 p-4">
|
||||
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
||||
{previewDownloadUrl && (
|
||||
|
|
@ -425,7 +632,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|||
|
||||
<div className="flex-shrink-0 pr-24">
|
||||
<div className="text-15 font-semibold text-primary">{fullFileName}</div>
|
||||
<div className="mt-1 text-12 text-tertiary">{convertBytesToSize(attachment.attributes.size)}</div>
|
||||
<div className="mt-1 text-12 text-tertiary">{convertBytesToSize(size)}</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 min-h-0 flex-1 overflow-hidden rounded-2xl bg-black/20">
|
||||
|
|
@ -493,7 +700,5 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,16 +6,14 @@
|
|||
|
||||
import { useRef, type ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { MoveDiagonal, MoveRight } from "lucide-react";
|
||||
import { Maximize2, Minimize2, MoveRight, X } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CenterPanelIcon, CopyLinkIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
|
||||
import { CopyLinkIcon } 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 { SelectionDropdown } from "@/components/common/selection-dropdown";
|
||||
import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
|
|
@ -31,24 +29,6 @@ import { IconButton } from "@plane/propel/icon-button";
|
|||
|
||||
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;
|
||||
|
|
@ -68,7 +48,6 @@ export type PeekOverviewHeaderProps = {
|
|||
actionSlot?: ReactNode;
|
||||
metaSlot?: ReactNode;
|
||||
showCopyLink?: boolean;
|
||||
showLayoutSwitcher?: boolean;
|
||||
showQuickActions?: boolean;
|
||||
showSubscription?: boolean;
|
||||
};
|
||||
|
|
@ -82,7 +61,6 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
|||
issueId,
|
||||
isArchived,
|
||||
disabled,
|
||||
embedIssue = false,
|
||||
removeRoutePeekId,
|
||||
toggleDeleteIssueModal,
|
||||
toggleArchiveIssueModal,
|
||||
|
|
@ -93,7 +71,6 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
|||
actionSlot,
|
||||
metaSlot,
|
||||
showCopyLink = true,
|
||||
showLayoutSwitcher = !embedIssue,
|
||||
showQuickActions = true,
|
||||
showSubscription = true,
|
||||
} = props;
|
||||
|
|
@ -113,7 +90,6 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
|||
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 },
|
||||
|
|
@ -168,50 +144,29 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
|||
<div
|
||||
ref={parentRef}
|
||||
className={`relative flex flex-wrap items-start justify-between gap-3 px-6 py-5 ${
|
||||
currentMode?.key === "full-screen" ? "border-b border-subtle/70" : ""
|
||||
peekMode === "full-screen" ? "border-b border-subtle/70" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-3 md:gap-4">
|
||||
<Tooltip tooltipContent={t("common.close_peek_view")} isMobile={isMobile}>
|
||||
<button onClick={removeRoutePeekId}>
|
||||
{peekMode === "side-peek" ? (
|
||||
<MoveRight className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||
) : (
|
||||
<X className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip tooltipContent={t("issue.open_in_full_screen")} isMobile={isMobile}>
|
||||
<Link href={workItemLink} onClick={() => removeRoutePeekId()}>
|
||||
<MoveDiagonal className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
{currentMode && showLayoutSwitcher && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<SelectionDropdown
|
||||
menuButton={
|
||||
<Tooltip tooltipContent={t("common.toggle_peek_view_layout")} isMobile={isMobile}>
|
||||
<span>
|
||||
<currentMode.icon className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
menuButtonWrapperClassName="flex items-center"
|
||||
options={PEEK_OPTIONS.map((mode) => ({
|
||||
key: mode.key,
|
||||
isChecked: currentMode.key === mode.key,
|
||||
onClick: () => setPeekMode(mode.key),
|
||||
title: (
|
||||
<div
|
||||
className={`flex items-center gap-1.5 ${
|
||||
currentMode.key === mode.key ? "text-secondary" : "text-placeholder hover:text-secondary"
|
||||
}`}
|
||||
>
|
||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
||||
{t(mode.i18n_title)}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<button type="button" onClick={() => setPeekMode(peekMode === "full-screen" ? "side-peek" : "full-screen")}>
|
||||
{peekMode === "full-screen" ? (
|
||||
<Minimize2 className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||
) : (
|
||||
<Maximize2 className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{metaSlot}
|
||||
</div>
|
||||
<div className="ml-auto flex min-w-0 shrink-0 flex-wrap items-center justify-end gap-x-4 gap-y-2">
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
|||
const issue = getIssueById(issueId);
|
||||
const shouldUseInteractiveEmbeddedLayout = embedIssue && interactiveEmbeddedLayout;
|
||||
const shouldRenderPeekSurface = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
||||
const shouldAllowPeekModeToggle = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
||||
const shouldAllowPeekResize = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
||||
// remove peek id
|
||||
const removeRoutePeekId = () => {
|
||||
|
|
@ -371,8 +370,8 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
|||
!shouldUseBeamPeekShell && {
|
||||
"nodedc-peek-side-bound top-[5.35rem] right-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:max-w-[calc(100vw-1.5rem)] md:min-w-[640px]":
|
||||
peekMode === "side-peek",
|
||||
"top-[8.33%] left-[8.33%] size-5/6 rounded-[28px]": peekMode === "modal",
|
||||
"absolute inset-0 m-4 rounded-[28px]": peekMode === "full-screen",
|
||||
"nodedc-peek-modal-bound rounded-[28px]": peekMode === "modal",
|
||||
"nodedc-peek-full-bound rounded-[28px]": peekMode === "full-screen",
|
||||
},
|
||||
!embedIssue &&
|
||||
shouldUseBeamPeekShell &&
|
||||
|
|
@ -448,7 +447,6 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
|||
isSubmitting={isSubmitting}
|
||||
disabled={disabled}
|
||||
embedIssue={embedIssue}
|
||||
showLayoutSwitcher={shouldAllowPeekModeToggle}
|
||||
/>
|
||||
)}
|
||||
{/* content */}
|
||||
|
|
|
|||
|
|
@ -386,6 +386,31 @@
|
|||
bottom: var(--nodedc-peek-bottom-offset) !important;
|
||||
}
|
||||
|
||||
.nodedc-peek-modal-bound {
|
||||
position: fixed !important;
|
||||
top: 5.35rem !important;
|
||||
bottom: var(--nodedc-peek-bottom-offset) !important;
|
||||
left: 50% !important;
|
||||
right: auto !important;
|
||||
width: min(76rem, calc(100vw - 3rem)) !important;
|
||||
max-width: calc(100vw - 3rem) !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nodedc-peek-full-bound {
|
||||
position: fixed !important;
|
||||
top: 5.35rem !important;
|
||||
right: 0.75rem !important;
|
||||
bottom: var(--nodedc-peek-bottom-offset) !important;
|
||||
left: 0.75rem !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.nodedc-beam-peek-shell[data-fullscreen="true"] {
|
||||
grid-template-columns: minmax(0, 1fr) 0;
|
||||
column-gap: 0;
|
||||
|
|
@ -5806,6 +5831,23 @@
|
|||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.nodedc-attachments-panel[data-view-mode="list"] {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.nodedc-attachments-list-scroll {
|
||||
min-height: 13.5rem;
|
||||
max-height: min(34rem, calc(100vh - 20rem));
|
||||
resize: vertical;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.nodedc-attachments-list-scroll::-webkit-resizer {
|
||||
background:
|
||||
linear-gradient(135deg, transparent 0 45%, rgba(var(--nodedc-accent-rgb), 0.65) 46% 54%, transparent 55%),
|
||||
linear-gradient(135deg, transparent 0 62%, rgba(255, 255, 255, 0.22) 63% 70%, transparent 71%);
|
||||
}
|
||||
|
||||
.nodedc-processing-loader,
|
||||
.nodedc-voice-task-processing-loader {
|
||||
--nodedc-processing-loader-rgb: var(--nodedc-accent-rgb);
|
||||
|
|
|
|||
Loading…
Reference in New Issue