From df4767a085ee4dc20391748b1647a38962a50aa6 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Tue, 23 Jun 2026 11:32:49 +0300 Subject: [PATCH] Sync BIM attachment versions from viewer storage --- .../attachment/attachment-list-item.tsx | 41 ++++++++++-- .../issue/issue_attachment.service.ts | 1 + plane-src/apps/web/helpers/beam-viewer.ts | 67 ++++++++++++++++++- .../types/src/issues/issue_attachment.ts | 4 ++ 4 files changed, 105 insertions(+), 8 deletions(-) diff --git a/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx b/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx index fb51785..a0efbd0 100644 --- a/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx +++ b/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx @@ -28,10 +28,12 @@ import { buildBeamViewerUrl, dispatchBeamViewerOpenEvent, fetchBeamConversionStatus, + fetchBeamModelVersions, getBeamModelVersionRecords, getBeamVersionViewerUrl, getBeamViewerAttachment, isBeamModelFile, + mergeBeamModelVersionRecordLists, syncCurrentBeamVersionRecord, type TBeamModelVersionRecord, type TBeamViewerAttachment, @@ -85,7 +87,8 @@ const withBeamViewerSettingsSrc = ( url: string | null | undefined, settingsSrc: string | undefined ): string | undefined => { - if (!url || !settingsSrc) return url; + if (!url) return undefined; + if (!settingsSrc) return url; try { const parsedUrl = new URL(url, "http://localhost"); if (!parsedUrl.searchParams.has("settingsSrc")) parsedUrl.searchParams.set("settingsSrc", settingsSrc); @@ -241,9 +244,14 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt const [deletingVersionKey, setDeletingVersionKey] = useState(null); const [isVersionUploading, setIsVersionUploading] = useState(false); const [isThumbnailError, setIsThumbnailError] = useState(false); + const [liveBeamVersions, setLiveBeamVersions] = useState([]); const versionUploadInputRef = useRef(null); - const beamVersions = useMemo(() => getBeamModelVersionRecords(beamViewer, attachment), [attachment, beamViewer]); - const rawVersion = beamViewer?.version ?? attachment?.attributes.version; + const localBeamVersions = useMemo(() => getBeamModelVersionRecords(beamViewer, attachment), [attachment, beamViewer]); + const beamVersions = useMemo( + () => mergeBeamModelVersionRecordLists(localBeamVersions, liveBeamVersions), + [localBeamVersions, liveBeamVersions] + ); + const rawVersion = beamViewer?.version ?? (attachment?.attributes as { version?: number | string } | undefined)?.version; const versionLabel = typeof rawVersion === "number" ? `v${rawVersion}` @@ -414,6 +422,29 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt /> ) : null; + useEffect(() => { + if (!beamViewer?.src) { + setLiveBeamVersions([]); + return; + } + + let isMounted = true; + fetchBeamModelVersions(beamViewer) + .then((history) => { + if (!isMounted) return; + setLiveBeamVersions(Array.isArray(history.versions) ? history.versions : []); + }) + .catch((error) => { + if (!isMounted) return; + console.warn("BIM version history unavailable:", error); + setLiveBeamVersions([]); + }); + + return () => { + isMounted = false; + }; + }, [beamViewer?.assetId, beamViewer?.projectId, beamViewer?.src, beamViewer?.versionId]); + useEffect(() => { if (!beamViewer || !beamViewer.src) return; if (beamViewer.conversion?.status === "ready" && storedModelViewerUrl) return; @@ -973,8 +1004,8 @@ const BeamVersionHistoryModal = (props: TBeamVersionHistoryModal) => {
Статус
Действия
- {versions - .toSorted((first, second) => second.version - first.version) + {[...versions] + .sort((first, second) => second.version - first.version) .map((version) => { const viewerUrl = getBeamVersionViewerUrl(version); const isCurrent = currentVersionId diff --git a/plane-src/apps/web/core/services/issue/issue_attachment.service.ts b/plane-src/apps/web/core/services/issue/issue_attachment.service.ts index 81a10a5..f9898d2 100644 --- a/plane-src/apps/web/core/services/issue/issue_attachment.service.ts +++ b/plane-src/apps/web/core/services/issue/issue_attachment.service.ts @@ -148,6 +148,7 @@ export class IssueAttachmentService extends APIService { { ...nextBeamViewer, assetId, + projectId: nextBeamViewer.projectId || previousBeamViewer.projectId, previewAvailable: nextBeamViewer.previewAvailable, uploadedBy, version: nextVersion, diff --git a/plane-src/apps/web/helpers/beam-viewer.ts b/plane-src/apps/web/helpers/beam-viewer.ts index cc68dc3..4e08aa7 100644 --- a/plane-src/apps/web/helpers/beam-viewer.ts +++ b/plane-src/apps/web/helpers/beam-viewer.ts @@ -21,9 +21,12 @@ export type TBeamModelVersionRecord = { downloadUrl: string; originalFilename: string; previewAvailable: boolean; + projectId?: string; sha256?: string; size: number; + sourceSrc?: string; src: string; + status?: string; type: string; uploadedBy?: string; uploadedAt: string; @@ -39,6 +42,7 @@ export type TBeamViewerAttachment = { downloadUrl: string; originalFilename: string; previewAvailable: boolean; + projectId?: string; sha256?: string; src: string; type: string; @@ -72,6 +76,16 @@ export type TBeamConversionStatus = { versionId?: string; }; +export type TBeamVersionHistoryResponse = { + assetId?: string; + currentVersion?: number; + currentVersionId?: string; + ok: boolean; + projectId?: string; + updatedAt?: string; + versions: TBeamModelVersionRecord[]; +}; + export const BEAM_VIEWER_OPEN_EVENT = "nodedc:beam-viewer-open"; export type TBeamViewerOpenEventDetail = { @@ -192,7 +206,7 @@ export const getBeamViewerAttachment = (attachment: TIssueAttachment | undefined }; const getAttachmentVersionNumber = (attachment: TIssueAttachment | undefined): number => { - const rawVersion = attachment?.attributes.version; + const rawVersion = (attachment?.attributes as { version?: number | string } | undefined)?.version; if (typeof rawVersion === "number" && Number.isFinite(rawVersion)) return rawVersion; if (typeof rawVersion === "string") { const parsed = Number.parseInt(rawVersion.replace(/^v/i, ""), 10); @@ -222,9 +236,12 @@ export const createBeamModelVersionRecord = ( downloadUrl: beamViewer.downloadUrl, originalFilename: beamViewer.originalFilename, previewAvailable: beamViewer.previewAvailable, + projectId: beamViewer.projectId, sha256: beamViewer.sha256, size: options.size ?? beamViewer.conversion?.size ?? Number(options.attachment?.attributes.size ?? 0), + sourceSrc: beamViewer.conversion?.sourceSrc || beamViewer.src, src: beamViewer.src, + status: beamViewer.conversion?.status || "ready", type: beamViewer.type, uploadedBy: beamViewer.uploadedBy || options.uploadedBy || options.attachment?.created_by, uploadedAt: beamViewer.uploadedAt, @@ -251,7 +268,7 @@ export const getBeamModelVersionRecords = ( return versions .filter((version) => typeof version?.version === "number" && Number.isFinite(version.version)) - .toSorted((first, second) => first.version - second.version); + .sort((first, second) => first.version - second.version); }; export const mergeBeamModelVersionRecords = ( @@ -268,11 +285,26 @@ export const mergeBeamModelVersionRecords = ( ) || nextBeamViewer.versions?.[0] || createBeamModelVersionRecord(nextBeamViewer); - return [...previousVersions.filter((version) => !isSameBeamVersionRecord(version, nextRecord)), nextRecord].toSorted( + return [...previousVersions.filter((version) => !isSameBeamVersionRecord(version, nextRecord)), nextRecord].sort( (first, second) => first.version - second.version ); }; +export const mergeBeamModelVersionRecordLists = ( + firstVersions: TBeamModelVersionRecord[] = [], + secondVersions: TBeamModelVersionRecord[] = [] +): TBeamModelVersionRecord[] => { + const byKey = new Map(); + [...firstVersions, ...secondVersions].forEach((version) => { + const key = version.versionId || `version-${version.version}`; + const previous = byKey.get(key); + byKey.set(key, { ...(previous || {}), ...version }); + }); + return [...byKey.values()] + .filter((version) => typeof version?.version === "number" && Number.isFinite(version.version)) + .sort((first, second) => first.version - second.version); +}; + export const syncCurrentBeamVersionRecord = ( beamViewer: TBeamViewerAttachment, attachment?: TIssueAttachment @@ -287,6 +319,14 @@ export const syncCurrentBeamVersionRecord = ( export const getBeamVersionViewerUrl = (version: TBeamModelVersionRecord): string | undefined => { if (version.viewerUrl) return version.viewerUrl; + if (!version.conversion && version.previewAvailable && version.src) { + return buildBeamViewerUrl({ + name: version.originalFilename, + settingsSrc: version.sourceSrc || version.src, + src: version.src, + type: version.type, + }); + } if (version.conversion?.status !== "ready" || !version.conversion.artifactSrc) return undefined; const artifactType = version.conversion.artifactType || version.conversion.targetFormat || "gltf"; return buildBeamViewerUrl({ @@ -315,6 +355,25 @@ export const fetchBeamConversionStatus = async (beamViewer: TBeamViewerAttachmen }; }; +export const fetchBeamModelVersions = async ( + beamViewer: TBeamViewerAttachment +): Promise => { + const versionsUrl = new URL("/api/uploads/versions", `${getBeamApiBaseUrl()}/`); + if (beamViewer.projectId && beamViewer.assetId) { + versionsUrl.searchParams.set("projectId", beamViewer.projectId); + versionsUrl.searchParams.set("assetId", beamViewer.assetId); + } else { + versionsUrl.searchParams.set("src", toBeamRelativeUploadSrc(beamViewer.src)); + } + + const response = await fetch(versionsUrl.toString(), { credentials: "include" }); + if (!response.ok) { + throw new Error(`BIM version history failed: HTTP ${response.status}`); + } + + return (await response.json()) as TBeamVersionHistoryResponse; +}; + export const uploadBeamModelFile = ( file: File, options: { @@ -369,6 +428,7 @@ export const uploadBeamModelFile = ( assetId?: string; conversion?: TBeamViewerAttachment["conversion"]; originalFilename?: string; + projectId?: string; sha256?: string; src?: string; uploadedAt?: string; @@ -387,6 +447,7 @@ export const uploadBeamModelFile = ( downloadUrl: downloadUrl.toString(), originalFilename: response.originalFilename || file.name, previewAvailable: !!directViewerType, + projectId: response.projectId, sha256: response.sha256, src: downloadUrl.toString(), type, diff --git a/plane-src/packages/types/src/issues/issue_attachment.ts b/plane-src/packages/types/src/issues/issue_attachment.ts index 9966448..b736389 100644 --- a/plane-src/packages/types/src/issues/issue_attachment.ts +++ b/plane-src/packages/types/src/issues/issue_attachment.ts @@ -26,9 +26,12 @@ export type TBeamModelVersionRecord = { downloadUrl: string; originalFilename: string; previewAvailable: boolean; + projectId?: string; sha256?: string; size: number; + sourceSrc?: string; src: string; + status?: string; type: string; uploadedBy?: string; uploadedAt: string; @@ -44,6 +47,7 @@ export type TBeamViewerAttachment = { downloadUrl: string; originalFilename: string; previewAvailable: boolean; + projectId?: string; sha256?: string; src: string; type: string;