beam: refine model attachment status icon

This commit is contained in:
DCCONSTRUCTIONS 2026-06-04 22:49:43 +03:00
parent 3e41c451c1
commit 677916389f
1 changed files with 63 additions and 20 deletions

View File

@ -15,9 +15,9 @@ import type { TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types"; import { EIssueServiceType } from "@plane/types";
// ui // ui
import type { TContextMenuItem } from "@plane/ui"; import type { TContextMenuItem } from "@plane/ui";
import { ActionDropdown, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; import { ActionDropdown, EModalPosition, EModalWidth, ModalCore, Spinner } from "@plane/ui";
import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils"; import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils";
import { Box, Download, Eye, ImageIcon, Play, X } from "lucide-react"; import { AlertTriangle, Box, Download, Eye, ImageIcon, Play, X } from "lucide-react";
// components // components
// //
import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
@ -90,6 +90,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
const previewURL = appendSearchParam(fileURL, "preview", "true"); const previewURL = appendSearchParam(fileURL, "preview", "true");
const beamViewer = getBeamViewerAttachment(attachment); const beamViewer = getBeamViewerAttachment(attachment);
const [beamConversionStatus, setBeamConversionStatus] = useState<TBeamConversionStatus | null>(null); const [beamConversionStatus, setBeamConversionStatus] = useState<TBeamConversionStatus | null>(null);
const [beamConversionError, setBeamConversionError] = useState<string | null>(null);
const resolvedArtifactUrl = beamConversionStatus?.status === "ready" ? beamConversionStatus.artifactUrl : undefined; const resolvedArtifactUrl = beamConversionStatus?.status === "ready" ? beamConversionStatus.artifactUrl : undefined;
const modelViewerUrl = const modelViewerUrl =
beamViewer?.viewerUrl || beamViewer?.viewerUrl ||
@ -102,11 +103,22 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
: undefined); : undefined);
const modelDownloadUrl = beamViewer?.downloadUrl || beamViewer?.src; const modelDownloadUrl = beamViewer?.downloadUrl || beamViewer?.src;
const previewDownloadUrl = modelDownloadUrl || fileURL; const previewDownloadUrl = modelDownloadUrl || fileURL;
const beamConversionStatusLabel = getBeamConversionStatusLabel(
beamConversionStatus?.status ?? beamViewer?.conversion?.status
);
const isBeamAwaitingPreview = !!beamViewer && !modelViewerUrl;
const isBeamModel = isBeamModelFile(fullFileName); const isBeamModel = isBeamModelFile(fullFileName);
const beamEffectiveStatus = beamConversionStatus?.status ?? beamViewer?.conversion?.status;
const beamConversionStatusLabel = getBeamConversionStatusLabel(beamEffectiveStatus);
const beamStatusErrorMessage =
beamConversionError ||
beamConversionStatus?.error ||
(beamConversionStatus?.status === "ready" && !modelViewerUrl
? "Beam вернул статус готовности, но не вернул viewer-артефакт."
: null);
const isBeamConversionFailed = beamEffectiveStatus === "failed" || !!beamStatusErrorMessage;
const isBeamAwaitingPreview = !!beamViewer && !modelViewerUrl && !isBeamConversionFailed;
const beamStatusTooltipContent = isBeamConversionFailed
? beamStatusErrorMessage || beamConversionStatus?.message || "Ошибка подготовки дерева компонентов."
: beamConversionStatus?.message ||
beamViewer?.conversion?.message ||
"Оригинальная модель загружена в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов.";
const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [isModelViewerOpen, setIsModelViewerOpen] = useState(false); const [isModelViewerOpen, setIsModelViewerOpen] = useState(false);
const [isThumbnailError, setIsThumbnailError] = useState(false); const [isThumbnailError, setIsThumbnailError] = useState(false);
@ -148,21 +160,23 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
useEffect(() => { useEffect(() => {
if (!beamViewer || beamViewer.viewerUrl || !beamViewer.conversion) return; if (!beamViewer || beamViewer.viewerUrl || !beamViewer.src) return;
let isMounted = true; let isMounted = true;
let timeoutId: ReturnType<typeof window.setTimeout> | undefined; let timeoutId: number | undefined;
const pollStatus = async () => { const pollStatus = async () => {
try { try {
const status = await fetchBeamConversionStatus(beamViewer); const status = await fetchBeamConversionStatus(beamViewer);
if (!isMounted) return; if (!isMounted) return;
setBeamConversionError(null);
setBeamConversionStatus(status); setBeamConversionStatus(status);
if (status.status !== "ready" && status.status !== "failed") { if (status.status !== "ready" && status.status !== "failed") {
timeoutId = window.setTimeout(pollStatus, 5000); timeoutId = window.setTimeout(pollStatus, 5000);
} }
} catch (_error) { } catch (error) {
if (!isMounted) return; if (!isMounted) return;
setBeamConversionError(error instanceof Error ? error.message : "Не удалось получить статус Beam.");
timeoutId = window.setTimeout(pollStatus, 8000); timeoutId = window.setTimeout(pollStatus, 8000);
} }
}; };
@ -211,7 +225,25 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
) : ( ) : (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
{isBeamModel ? ( {isBeamModel ? (
isBeamConversionFailed ? (
<Tooltip tooltipContent={beamStatusTooltipContent} isMobile={isMobile}>
<div className="grid size-9 place-items-center text-red-400">
<AlertTriangle className="size-9" />
</div>
</Tooltip>
) : isBeamAwaitingPreview ? (
<Tooltip tooltipContent={beamStatusTooltipContent} isMobile={isMobile}>
<div className="grid size-9 place-items-center">
<Spinner
height="36px"
width="36px"
className="fill-[rgb(var(--nodedc-accent-rgb))] text-[rgba(var(--nodedc-accent-rgb),0.18)]"
/>
</div>
</Tooltip>
) : (
<Box className="size-9 text-[rgb(var(--nodedc-accent-rgb))]" /> <Box className="size-9 text-[rgb(var(--nodedc-accent-rgb))]" />
)
) : previewType === "file" ? ( ) : previewType === "file" ? (
fileIcon fileIcon
) : ( ) : (
@ -231,11 +263,6 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
<span className="size-1 rounded-full bg-layer-1" /> <span className="size-1 rounded-full bg-layer-1" />
<span>{convertBytesToSize(attachment.attributes.size)}</span> <span>{convertBytesToSize(attachment.attributes.size)}</span>
</div> </div>
{beamConversionStatusLabel && (
<div className="mt-2 w-fit whitespace-nowrap rounded-full bg-[rgba(var(--nodedc-accent-rgb),0.12)] px-2 py-0.5 text-10 font-semibold text-[rgb(var(--nodedc-accent-rgb))]">
{beamConversionStatusLabel}
</div>
)}
</div> </div>
{attachment?.created_by && ( {attachment?.created_by && (
@ -311,17 +338,33 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
</div> </div>
) : previewType === "pdf" && previewURL ? ( ) : previewType === "pdf" && previewURL ? (
<IssueAttachmentPdfPreview fileURL={previewURL} /> <IssueAttachmentPdfPreview fileURL={previewURL} />
) : isBeamAwaitingPreview ? ( ) : isBeamAwaitingPreview || isBeamConversionFailed ? (
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center"> <div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
<div className="grid size-20 place-items-center rounded-3xl bg-[rgba(var(--nodedc-accent-rgb),0.12)] text-[rgb(var(--nodedc-accent-rgb))]"> <div
<Box className="size-10" /> className={`grid size-20 place-items-center rounded-3xl ${
isBeamConversionFailed
? "bg-red-500/10 text-red-400"
: "bg-[rgba(var(--nodedc-accent-rgb),0.12)] text-[rgb(var(--nodedc-accent-rgb))]"
}`}
>
{isBeamConversionFailed ? (
<AlertTriangle className="size-10" />
) : (
<Spinner
height="40px"
width="40px"
className="fill-[rgb(var(--nodedc-accent-rgb))] text-[rgba(var(--nodedc-accent-rgb),0.18)]"
/>
)}
</div> </div>
<div className="max-w-md"> <div className="max-w-md">
<div className="text-15 font-semibold text-primary"> <div className="text-15 font-semibold text-primary">
{beamConversionStatusLabel ?? "Ожидает дерево"} {isBeamConversionFailed ? "Ошибка дерева" : beamConversionStatusLabel ?? "Ожидает дерево"}
</div> </div>
<div className="mt-2 text-14 text-secondary"> <div className="mt-2 text-14 text-secondary">
Оригинальный STEP загружен в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов. {isBeamConversionFailed
? beamStatusTooltipContent
: "Оригинальный STEP загружен в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов."}
</div> </div>
</div> </div>
{modelDownloadUrl && ( {modelDownloadUrl && (