beam: persist ready viewer attachments
This commit is contained in:
parent
677916389f
commit
31dca29560
|
|
@ -257,6 +257,25 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
|||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def patch(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
beam_viewer = request.data.get("beamViewer")
|
||||
if isinstance(beam_viewer, dict):
|
||||
attributes = issue_attachment.attributes or {}
|
||||
existing_beam_viewer = attributes.get("beamViewer")
|
||||
if not isinstance(existing_beam_viewer, dict):
|
||||
return Response(
|
||||
{"error": "The attachment is not a Beam Viewer reference.", "status": False},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
attributes["beamViewer"] = {
|
||||
**existing_beam_viewer,
|
||||
**beam_viewer,
|
||||
}
|
||||
issue_attachment.attributes = attributes
|
||||
issue_attachment.is_uploaded = True
|
||||
issue_attachment.save(update_fields=["attributes", "is_uploaded", "updated_at"])
|
||||
return Response(IssueAttachmentSerializer(issue_attachment).data, status=status.HTTP_200_OK)
|
||||
|
||||
serializer = IssueAttachmentSerializer(issue_attachment)
|
||||
if not attachment_object_exists(issue_attachment):
|
||||
return Response(
|
||||
|
|
|
|||
|
|
@ -189,7 +189,10 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
|||
key={attachmentId}
|
||||
attachmentId={attachmentId}
|
||||
disabled={disabled}
|
||||
issueId={issueId}
|
||||
issueServiceType={issueServiceType}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,23 @@ export class IssueAttachmentService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async updateBeamIssueAttachmentReference(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
attachmentId: string,
|
||||
beamViewer: TBeamViewerAttachment
|
||||
): Promise<TIssueAttachment> {
|
||||
return this.patch(
|
||||
`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/${attachmentId}/`,
|
||||
{ beamViewer }
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data ?? error;
|
||||
});
|
||||
}
|
||||
|
||||
async uploadIssueAttachment(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@ import type { TIssueAttachment } from "@plane/types";
|
|||
export type TBeamViewerAttachment = {
|
||||
backend: "beam-viewer-local" | "beam-viewer";
|
||||
conversion?: {
|
||||
artifactSrc?: string;
|
||||
artifactType?: string;
|
||||
componentTreeRequired: boolean;
|
||||
message?: string;
|
||||
metadataSrc?: string;
|
||||
sourceFormat: string;
|
||||
sourceSrc?: string;
|
||||
status: "conversion_required" | "processing" | "ready" | "failed";
|
||||
targetFormat: "glb" | "xkt";
|
||||
updatedAt?: string;
|
||||
};
|
||||
downloadUrl: string;
|
||||
originalFilename: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue