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";
// 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 { Box, Download, Eye, ImageIcon, Play, X } from "lucide-react";
import { AlertTriangle, Box, Download, Eye, ImageIcon, Play, X } from "lucide-react";
// components
//
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
@ -90,6 +90,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
const previewURL = appendSearchParam(fileURL, "preview", "true");
const beamViewer = getBeamViewerAttachment(attachment);
const [beamConversionStatus, setBeamConversionStatus] = useState<TBeamConversionStatus | null>(null);
const [beamConversionError, setBeamConversionError] = useState<string | null>(null);
const resolvedArtifactUrl = beamConversionStatus?.status === "ready" ? beamConversionStatus.artifactUrl : undefined;
const modelViewerUrl =
beamViewer?.viewerUrl ||
@ -102,11 +103,22 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
: undefined);
const modelDownloadUrl = beamViewer?.downloadUrl || beamViewer?.src;
const previewDownloadUrl = modelDownloadUrl || fileURL;
const beamConversionStatusLabel = getBeamConversionStatusLabel(
beamConversionStatus?.status ?? beamViewer?.conversion?.status
);
const isBeamAwaitingPreview = !!beamViewer && !modelViewerUrl;
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 [isModelViewerOpen, setIsModelViewerOpen] = useState(false);
const [isThumbnailError, setIsThumbnailError] = useState(false);
@ -148,21 +160,23 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
const { isMobile } = usePlatformOS();
useEffect(() => {
if (!beamViewer || beamViewer.viewerUrl || !beamViewer.conversion) return;
if (!beamViewer || beamViewer.viewerUrl || !beamViewer.src) return;
let isMounted = true;
let timeoutId: ReturnType<typeof window.setTimeout> | undefined;
let timeoutId: number | undefined;
const pollStatus = async () => {
try {
const status = await fetchBeamConversionStatus(beamViewer);
if (!isMounted) return;
setBeamConversionError(null);
setBeamConversionStatus(status);
if (status.status !== "ready" && status.status !== "failed") {
timeoutId = window.setTimeout(pollStatus, 5000);
}
} catch (_error) {
} catch (error) {
if (!isMounted) return;
setBeamConversionError(error instanceof Error ? error.message : "Не удалось получить статус Beam.");
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">
{isBeamModel ? (
<Box className="size-9 text-[rgb(var(--nodedc-accent-rgb))]" />
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))]" />
)
) : previewType === "file" ? (
fileIcon
) : (
@ -231,11 +263,6 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
<span className="size-1 rounded-full bg-layer-1" />
<span>{convertBytesToSize(attachment.attributes.size)}</span>
</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>
{attachment?.created_by && (
@ -311,17 +338,33 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
</div>
) : previewType === "pdf" && 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="grid size-20 place-items-center rounded-3xl bg-[rgba(var(--nodedc-accent-rgb),0.12)] text-[rgb(var(--nodedc-accent-rgb))]">
<Box className="size-10" />
<div
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 className="max-w-md">
<div className="text-15 font-semibold text-primary">
{beamConversionStatusLabel ?? "Ожидает дерево"}
{isBeamConversionFailed ? "Ошибка дерева" : beamConversionStatusLabel ?? "Ожидает дерево"}
</div>
<div className="mt-2 text-14 text-secondary">
Оригинальный STEP загружен в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов.
{isBeamConversionFailed
? beamStatusTooltipContent
: "Оригинальный STEP загружен в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов."}
</div>
</div>
{modelDownloadUrl && (