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])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
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)
|
serializer = IssueAttachmentSerializer(issue_attachment)
|
||||||
if not attachment_object_exists(issue_attachment):
|
if not attachment_object_exists(issue_attachment):
|
||||||
return Response(
|
return Response(
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,10 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
key={attachmentId}
|
key={attachmentId}
|
||||||
attachmentId={attachmentId}
|
attachmentId={attachmentId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
issueId={issueId}
|
||||||
issueServiceType={issueServiceType}
|
issueServiceType={issueServiceType}
|
||||||
|
projectId={projectId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
fetchBeamConversionStatus,
|
fetchBeamConversionStatus,
|
||||||
getBeamViewerAttachment,
|
getBeamViewerAttachment,
|
||||||
isBeamModelFile,
|
isBeamModelFile,
|
||||||
|
type TBeamViewerAttachment,
|
||||||
type TBeamConversionStatus,
|
type TBeamConversionStatus,
|
||||||
} from "@/helpers/beam-viewer";
|
} from "@/helpers/beam-viewer";
|
||||||
import { IssueAttachmentPdfPreview, IssueAttachmentPdfThumbnail } from "./attachment-pdf-preview";
|
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 { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
import { useMember } from "@/hooks/store/use-member";
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
import { IssueAttachmentService } from "@/services/issue";
|
||||||
|
|
||||||
type TIssueAttachmentsListItem = {
|
type TIssueAttachmentsListItem = {
|
||||||
attachmentId: string;
|
attachmentId: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
issueServiceType?: TIssueServiceType;
|
issueServiceType?: TIssueServiceType;
|
||||||
|
issueId: string;
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS = new Set(["apng", "avif", "bmp", "gif", "jpg", "jpeg", "png", "svg", "webp"]);
|
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}` : ""}`;
|
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 getPreviewType = (extension: string) => {
|
||||||
const normalizedExtension = extension.toLowerCase();
|
const normalizedExtension = extension.toLowerCase();
|
||||||
if (IMAGE_EXTENSIONS.has(normalizedExtension)) return "image";
|
if (IMAGE_EXTENSIONS.has(normalizedExtension)) return "image";
|
||||||
|
|
@ -69,14 +86,49 @@ const getBeamConversionStatusLabel = (status: string | undefined): string | null
|
||||||
return "Ожидает дерево";
|
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) {
|
export const IssueAttachmentsListItem = observer(function IssueAttachmentsListItem(props: TIssueAttachmentsListItem) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// props
|
// props
|
||||||
const { attachmentId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
|
const { attachmentId, disabled, issueId, issueServiceType = EIssueServiceType.ISSUES, projectId, workspaceSlug } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const {
|
const {
|
||||||
attachment: { getAttachmentById },
|
attachment: { getAttachmentById },
|
||||||
|
fetchAttachments,
|
||||||
toggleDeleteAttachmentModal,
|
toggleDeleteAttachmentModal,
|
||||||
} = useIssueDetail(issueServiceType);
|
} = useIssueDetail(issueServiceType);
|
||||||
// derived values
|
// derived values
|
||||||
|
|
@ -91,16 +143,17 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
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 [beamConversionError, setBeamConversionError] = useState<string | null>(null);
|
||||||
|
const attachmentService = useMemo(() => new IssueAttachmentService(issueServiceType), [issueServiceType]);
|
||||||
const resolvedArtifactUrl = beamConversionStatus?.status === "ready" ? beamConversionStatus.artifactUrl : undefined;
|
const resolvedArtifactUrl = beamConversionStatus?.status === "ready" ? beamConversionStatus.artifactUrl : undefined;
|
||||||
|
const storedModelViewerUrl = getUsableBeamViewerUrl(beamViewer?.viewerUrl);
|
||||||
const modelViewerUrl =
|
const modelViewerUrl =
|
||||||
beamViewer?.viewerUrl ||
|
|
||||||
(resolvedArtifactUrl
|
(resolvedArtifactUrl
|
||||||
? buildBeamViewerUrl({
|
? buildBeamViewerUrl({
|
||||||
name: fullFileName,
|
name: fullFileName,
|
||||||
src: resolvedArtifactUrl,
|
src: resolvedArtifactUrl,
|
||||||
type: beamConversionStatus?.artifactType || "gltf",
|
type: beamConversionStatus?.artifactType || "gltf",
|
||||||
})
|
})
|
||||||
: undefined);
|
: undefined) || storedModelViewerUrl;
|
||||||
const modelDownloadUrl = beamViewer?.downloadUrl || beamViewer?.src;
|
const modelDownloadUrl = beamViewer?.downloadUrl || beamViewer?.src;
|
||||||
const previewDownloadUrl = modelDownloadUrl || fileURL;
|
const previewDownloadUrl = modelDownloadUrl || fileURL;
|
||||||
const isBeamModel = isBeamModelFile(fullFileName);
|
const isBeamModel = isBeamModelFile(fullFileName);
|
||||||
|
|
@ -160,7 +213,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!beamViewer || beamViewer.viewerUrl || !beamViewer.src) return;
|
if (!beamViewer || storedModelViewerUrl || !beamViewer.src) return;
|
||||||
|
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
let timeoutId: number | undefined;
|
let timeoutId: number | undefined;
|
||||||
|
|
@ -171,6 +224,13 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setBeamConversionError(null);
|
setBeamConversionError(null);
|
||||||
setBeamConversionStatus(status);
|
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") {
|
if (status.status !== "ready" && status.status !== "failed") {
|
||||||
timeoutId = window.setTimeout(pollStatus, 5000);
|
timeoutId = window.setTimeout(pollStatus, 5000);
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +247,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
if (timeoutId) window.clearTimeout(timeoutId);
|
if (timeoutId) window.clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [beamViewer]);
|
}, [attachmentId, attachmentService, beamViewer, fetchAttachments, fullFileName, issueId, projectId, storedModelViewerUrl, workspaceSlug]);
|
||||||
|
|
||||||
if (!attachment) return <></>;
|
if (!attachment) return <></>;
|
||||||
|
|
||||||
|
|
@ -430,6 +490,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
</div>
|
</div>
|
||||||
{modelViewerUrl && (
|
{modelViewerUrl && (
|
||||||
<iframe
|
<iframe
|
||||||
|
key={modelViewerUrl}
|
||||||
src={modelViewerUrl}
|
src={modelViewerUrl}
|
||||||
title={`Beam Viewer: ${fullFileName}`}
|
title={`Beam Viewer: ${fullFileName}`}
|
||||||
tabIndex={-1}
|
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(
|
async uploadIssueAttachment(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,16 @@ import type { TIssueAttachment } from "@plane/types";
|
||||||
export type TBeamViewerAttachment = {
|
export type TBeamViewerAttachment = {
|
||||||
backend: "beam-viewer-local" | "beam-viewer";
|
backend: "beam-viewer-local" | "beam-viewer";
|
||||||
conversion?: {
|
conversion?: {
|
||||||
|
artifactSrc?: string;
|
||||||
|
artifactType?: string;
|
||||||
componentTreeRequired: boolean;
|
componentTreeRequired: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
metadataSrc?: string;
|
||||||
sourceFormat: string;
|
sourceFormat: string;
|
||||||
|
sourceSrc?: string;
|
||||||
status: "conversion_required" | "processing" | "ready" | "failed";
|
status: "conversion_required" | "processing" | "ready" | "failed";
|
||||||
targetFormat: "glb" | "xkt";
|
targetFormat: "glb" | "xkt";
|
||||||
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
originalFilename: string;
|
originalFilename: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue