mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
feat: move videos to another project (single + bulk)
Add a "Move to project" action in the video card dropdown and the
selection-mode toolbar. Videos (with their versions, comments, assets and
video-scoped share links) can be moved into another project in the same
workspace.
- New GET/POST /api/projects/[projectId]/videos/move: GET lists manageable
destination projects in the workspace; POST performs the move.
- Requires canEdit on both source and destination; same-workspace only.
- Move runs in an interactive transaction that re-asserts source ownership
atomically (updateMany guarded by projectId) to avoid a TOCTOU race, and
returns 409 on conflict. GET is rate-limited ('api').
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
Loader2,
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
FolderInput,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { VideoCard } from '@/components/video-card';
|
||||
import { VideoDragDropUploader } from '@/components/video-drag-drop-uploader';
|
||||
import { MoveVideosDialog } from '@/components/move-videos-dialog';
|
||||
import type { DirectUploadProvider } from '@/components/video-page/types';
|
||||
import {
|
||||
runProjectDownloadManifest,
|
||||
@@ -105,6 +107,7 @@ export function ProjectContentClient({
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [isDeletingSelected, setIsDeletingSelected] = useState(false);
|
||||
const [showDeleteSelectedDialog, setShowDeleteSelectedDialog] = useState(false);
|
||||
const [showMoveSelectedDialog, setShowMoveSelectedDialog] = useState(false);
|
||||
|
||||
const canSelectVideos = canDownloadProject || canEdit;
|
||||
|
||||
@@ -138,6 +141,13 @@ export function ProjectContentClient({
|
||||
setSelectedVideoIds((prev) => prev.filter((id) => id !== videoId));
|
||||
}, []);
|
||||
|
||||
const handleVideosMoved = useCallback((movedIds: string[]) => {
|
||||
const moved = new Set(movedIds);
|
||||
setLocalVideos((prev) => prev.filter((video) => !moved.has(video.id)));
|
||||
setSelectedVideoIds([]);
|
||||
setSelectionMode(false);
|
||||
}, []);
|
||||
|
||||
const toggleVideoSelection = useCallback((videoId: string, selected: boolean) => {
|
||||
setSelectedVideoIds((prev) => {
|
||||
if (selected) {
|
||||
@@ -447,6 +457,17 @@ export function ProjectContentClient({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowMoveSelectedDialog(true)}
|
||||
disabled={selectedCount === 0 || isDeletingSelected}
|
||||
>
|
||||
<FolderInput className="h-4 w-4 mr-2" />
|
||||
Move to project
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -561,6 +582,14 @@ export function ProjectContentClient({
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<MoveVideosDialog
|
||||
open={showMoveSelectedDialog}
|
||||
onOpenChange={setShowMoveSelectedDialog}
|
||||
projectId={projectId}
|
||||
videoIds={selectedVideoIds}
|
||||
onMoved={handleVideosMoved}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user