|
|
|
@@ -5,7 +5,7 @@
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { observer } from "mobx-react";
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
|
|
|
|
import { useTranslation } from "@plane/i18n";
|
|
|
|
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
|
|
fetchBeamConversionStatus,
|
|
|
|
|
getBeamViewerAttachment,
|
|
|
|
|
isBeamModelFile,
|
|
|
|
|
type TBeamViewerAttachment,
|
|
|
|
|
type TBeamConversionStatus,
|
|
|
|
|
} from "@/helpers/beam-viewer";
|
|
|
|
|
import { IssueAttachmentPdfPreview, IssueAttachmentPdfThumbnail } from "./attachment-pdf-preview";
|
|
|
|
@@ -35,11 +36,15 @@ import { IssueAttachmentPdfPreview, IssueAttachmentPdfThumbnail } from "./attach
|
|
|
|
|
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
|
|
|
|
import { useMember } from "@/hooks/store/use-member";
|
|
|
|
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
|
|
|
|
import { IssueAttachmentService } from "@/services/issue";
|
|
|
|
|
|
|
|
|
|
type TIssueAttachmentsListItem = {
|
|
|
|
|
attachmentId: string;
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
issueServiceType?: TIssueServiceType;
|
|
|
|
|
issueId: string;
|
|
|
|
|
projectId: string;
|
|
|
|
|
workspaceSlug: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const IMAGE_EXTENSIONS = new Set(["apng", "avif", "bmp", "gif", "jpg", "jpeg", "png", "svg", "webp"]);
|
|
|
|
@@ -53,6 +58,18 @@ const appendSearchParam = (url: string | undefined, key: string, value: string):
|
|
|
|
|
return `${urlWithoutHash}${separator}${key}=${encodeURIComponent(value)}${hash ? `#${hash}` : ""}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getUsableBeamViewerUrl = (url: string | undefined): string | undefined => {
|
|
|
|
|
if (!url) return undefined;
|
|
|
|
|
try {
|
|
|
|
|
const parsedUrl = new URL(url, "http://localhost");
|
|
|
|
|
return parsedUrl.searchParams.has("url") || parsedUrl.searchParams.has("src") || parsedUrl.searchParams.has("projectId")
|
|
|
|
|
? url
|
|
|
|
|
: undefined;
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getPreviewType = (extension: string) => {
|
|
|
|
|
const normalizedExtension = extension.toLowerCase();
|
|
|
|
|
if (IMAGE_EXTENSIONS.has(normalizedExtension)) return "image";
|
|
|
|
@@ -69,14 +86,49 @@ const getBeamConversionStatusLabel = (status: string | undefined): string | null
|
|
|
|
|
return "Ожидает дерево";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildSyncedBeamViewerAttachment = (
|
|
|
|
|
beamViewer: TBeamViewerAttachment,
|
|
|
|
|
status: TBeamConversionStatus,
|
|
|
|
|
fullFileName: string
|
|
|
|
|
): TBeamViewerAttachment | null => {
|
|
|
|
|
if (status.status !== "ready" || !status.artifactUrl) return null;
|
|
|
|
|
|
|
|
|
|
const artifactType = status.artifactType || status.targetFormat || "gltf";
|
|
|
|
|
const targetFormat: "glb" | "xkt" = status.targetFormat || (artifactType === "xkt" ? "xkt" : "glb");
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...beamViewer,
|
|
|
|
|
previewAvailable: true,
|
|
|
|
|
viewerUrl: buildBeamViewerUrl({
|
|
|
|
|
name: fullFileName,
|
|
|
|
|
src: status.artifactUrl,
|
|
|
|
|
type: artifactType,
|
|
|
|
|
}),
|
|
|
|
|
conversion: {
|
|
|
|
|
...(beamViewer.conversion ?? {}),
|
|
|
|
|
artifactSrc: status.artifactSrc,
|
|
|
|
|
artifactType,
|
|
|
|
|
componentTreeRequired: status.componentTreeRequired ?? beamViewer.conversion?.componentTreeRequired ?? true,
|
|
|
|
|
message: status.message || beamViewer.conversion?.message,
|
|
|
|
|
metadataSrc: status.metadataSrc,
|
|
|
|
|
sourceFormat: status.sourceFormat || beamViewer.conversion?.sourceFormat || beamViewer.type,
|
|
|
|
|
sourceSrc: status.sourceSrc || beamViewer.conversion?.sourceSrc,
|
|
|
|
|
status: "ready",
|
|
|
|
|
targetFormat,
|
|
|
|
|
updatedAt: status.updatedAt || beamViewer.conversion?.updatedAt,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const IssueAttachmentsListItem = observer(function IssueAttachmentsListItem(props: TIssueAttachmentsListItem) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
// props
|
|
|
|
|
const { attachmentId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
|
|
|
|
|
const { attachmentId, disabled, issueId, issueServiceType = EIssueServiceType.ISSUES, projectId, workspaceSlug } = props;
|
|
|
|
|
// store hooks
|
|
|
|
|
const { getUserDetails } = useMember();
|
|
|
|
|
const {
|
|
|
|
|
attachment: { getAttachmentById },
|
|
|
|
|
fetchAttachments,
|
|
|
|
|
toggleDeleteAttachmentModal,
|
|
|
|
|
} = useIssueDetail(issueServiceType);
|
|
|
|
|
// derived values
|
|
|
|
@@ -91,16 +143,17 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|
|
|
|
const beamViewer = getBeamViewerAttachment(attachment);
|
|
|
|
|
const [beamConversionStatus, setBeamConversionStatus] = useState<TBeamConversionStatus | null>(null);
|
|
|
|
|
const [beamConversionError, setBeamConversionError] = useState<string | null>(null);
|
|
|
|
|
const attachmentService = useMemo(() => new IssueAttachmentService(issueServiceType), [issueServiceType]);
|
|
|
|
|
const resolvedArtifactUrl = beamConversionStatus?.status === "ready" ? beamConversionStatus.artifactUrl : undefined;
|
|
|
|
|
const storedModelViewerUrl = getUsableBeamViewerUrl(beamViewer?.viewerUrl);
|
|
|
|
|
const modelViewerUrl =
|
|
|
|
|
beamViewer?.viewerUrl ||
|
|
|
|
|
(resolvedArtifactUrl
|
|
|
|
|
? buildBeamViewerUrl({
|
|
|
|
|
name: fullFileName,
|
|
|
|
|
src: resolvedArtifactUrl,
|
|
|
|
|
type: beamConversionStatus?.artifactType || "gltf",
|
|
|
|
|
})
|
|
|
|
|
: undefined);
|
|
|
|
|
: undefined) || storedModelViewerUrl;
|
|
|
|
|
const modelDownloadUrl = beamViewer?.downloadUrl || beamViewer?.src;
|
|
|
|
|
const previewDownloadUrl = modelDownloadUrl || fileURL;
|
|
|
|
|
const isBeamModel = isBeamModelFile(fullFileName);
|
|
|
|
@@ -160,7 +213,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|
|
|
|
const { isMobile } = usePlatformOS();
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!beamViewer || beamViewer.viewerUrl || !beamViewer.src) return;
|
|
|
|
|
if (!beamViewer || storedModelViewerUrl || !beamViewer.src) return;
|
|
|
|
|
|
|
|
|
|
let isMounted = true;
|
|
|
|
|
let timeoutId: number | undefined;
|
|
|
|
@@ -171,6 +224,13 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|
|
|
|
if (!isMounted) return;
|
|
|
|
|
setBeamConversionError(null);
|
|
|
|
|
setBeamConversionStatus(status);
|
|
|
|
|
const syncedBeamViewer = buildSyncedBeamViewerAttachment(beamViewer, status, fullFileName);
|
|
|
|
|
if (syncedBeamViewer && syncedBeamViewer.viewerUrl !== beamViewer.viewerUrl) {
|
|
|
|
|
attachmentService
|
|
|
|
|
.updateBeamIssueAttachmentReference(workspaceSlug, projectId, issueId, attachmentId, syncedBeamViewer)
|
|
|
|
|
.then(() => fetchAttachments(workspaceSlug, projectId, issueId))
|
|
|
|
|
.catch((error) => console.error("Error in syncing Beam attachment metadata:", error));
|
|
|
|
|
}
|
|
|
|
|
if (status.status !== "ready" && status.status !== "failed") {
|
|
|
|
|
timeoutId = window.setTimeout(pollStatus, 5000);
|
|
|
|
|
}
|
|
|
|
@@ -187,7 +247,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|
|
|
|
isMounted = false;
|
|
|
|
|
if (timeoutId) window.clearTimeout(timeoutId);
|
|
|
|
|
};
|
|
|
|
|
}, [beamViewer]);
|
|
|
|
|
}, [attachmentId, attachmentService, beamViewer, fetchAttachments, fullFileName, issueId, projectId, storedModelViewerUrl, workspaceSlug]);
|
|
|
|
|
|
|
|
|
|
if (!attachment) return <></>;
|
|
|
|
|
|
|
|
|
@@ -430,6 +490,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
|
|
|
|
</div>
|
|
|
|
|
{modelViewerUrl && (
|
|
|
|
|
<iframe
|
|
|
|
|
key={modelViewerUrl}
|
|
|
|
|
src={modelViewerUrl}
|
|
|
|
|
title={`Beam Viewer: ${fullFileName}`}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|