fix: address review feedback on JSON export/import

- Use storageService.loadAllMediaAssets() in exportProjectAsJSON() for
  complete asset coverage instead of in-memory-only getAssets()
- Validate schema_version on import, rejecting mismatched versions
- Reject empty scenes array during import validation
- Refactor ImportProjectButton to use existing useFileUpload hook
This commit is contained in:
Rene Zander 2026-04-13 10:04:59 +00:00
parent bf2caa8193
commit 2f68b3e906
2 changed files with 35 additions and 32 deletions

View File

@ -4,7 +4,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { KeyboardEvent, MouseEvent } from "react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import type { EditorCore } from "@/core";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
@ -15,6 +15,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { useEditor } from "@/hooks/use-editor";
import { useFileUpload } from "@/hooks/use-file-upload";
import { useProjectsStore } from "./store";
import type {
TProjectMetadata,
@ -532,45 +533,37 @@ function NewProjectButton() {
function ImportProjectButton() {
const editor = useEditor();
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const { openFilePicker, fileInputProps } = useFileUpload({
accept: ".json,.opencut.json",
multiple: false,
onFilesSelected: async (files) => {
const file = files[0];
if (!file) return;
try {
const json = await file.text();
const projectId = await editor.project.importProjectFromJSON({ json });
if (projectId) {
router.push(`/editor/${projectId}`);
try {
const json = await file.text();
const projectId = await editor.project.importProjectFromJSON({ json });
if (projectId) {
router.push(`/editor/${projectId}`);
}
} catch (error) {
toast.error("Failed to read file", {
description:
error instanceof Error ? error.message : "Please try again",
});
}
} catch (error) {
toast.error("Failed to read file", {
description:
error instanceof Error ? error.message : "Please try again",
});
}
// Reset the input so the same file can be re-selected
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
},
});
return (
<>
<input
ref={fileInputRef}
type="file"
accept=".json,.opencut.json"
className="hidden"
onChange={handleImport}
/>
<input {...fileInputProps} />
<Button
size="lg"
variant="outline"
className="flex px-5 md:px-6"
onClick={() => fileInputRef.current?.click()}
onClick={openFilePicker}
>
<HugeiconsIcon icon={Upload01Icon} className="size-4" />
<span className="text-sm font-medium hidden md:block">Import</span>

View File

@ -715,7 +715,9 @@ export class ProjectManager {
timelineViewState: project.timelineViewState,
};
const mediaAssets = this.editor.media.getAssets();
const mediaAssets = await storageService.loadAllMediaAssets({
projectId: project.metadata.id,
});
const mediaManifest: ExportedMediaManifestEntry[] = mediaAssets.map(
(asset) => ({
mediaId: asset.id,
@ -864,7 +866,8 @@ export class ProjectManager {
if (
!parsed.project ||
!parsed.project.metadata ||
!parsed.project.scenes
!parsed.project.scenes ||
parsed.project.scenes.length === 0
) {
toast.error("Invalid project file", {
description: "The file does not contain a valid OpenCut project.",
@ -872,6 +875,13 @@ export class ProjectManager {
return null;
}
if (parsed.schema_version !== EXPORT_SCHEMA_VERSION) {
toast.error("Incompatible project file", {
description: `Unsupported schema version ${parsed.schema_version}, expected ${EXPORT_SCHEMA_VERSION}.`,
});
return null;
}
const imported = parsed.project;
const newProjectId = generateUUID();