From 568e8850a29c8613667eb2ec2097afb0188d092a Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Fri, 3 Oct 2025 15:18:56 +0100
Subject: [PATCH 1/9] implement video folder movement functionality

---
 apps/web/actions/folders/getAllFolders.ts     |  35 +
 .../web/actions/folders/moveVideosToFolder.ts |  84 +++
 .../_components/FolderSelectionDialog.tsx     | 287 ++++++++
 .../caps/components/SelectedCapsBar.tsx       | 191 +++---
 apps/web/lib/folder.ts                        | 643 +++++++++++-------
 5 files changed, 911 insertions(+), 329 deletions(-)
 create mode 100644 apps/web/actions/folders/getAllFolders.ts
 create mode 100644 apps/web/actions/folders/moveVideosToFolder.ts
 create mode 100644 apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx

diff --git a/apps/web/actions/folders/getAllFolders.ts b/apps/web/actions/folders/getAllFolders.ts
new file mode 100644
index 0000000000..8474057688
--- /dev/null
+++ b/apps/web/actions/folders/getAllFolders.ts
@@ -0,0 +1,35 @@
+"use server";
+
+import { getCurrentUser } from "@cap/database/auth/session";
+import { CurrentUser } from "@cap/web-domain";
+import { Effect } from "effect";
+import { getAllFolders } from "../../lib/folder";
+import { runPromise } from "../../lib/server";
+
+export async function getAllFoldersAction(
+  root:
+    | { variant: "user" }
+    | { variant: "space"; spaceId: string }
+    | { variant: "org"; organizationId: string }
+) {
+  try {
+    const user = await getCurrentUser();
+    if (!user || !user.activeOrganizationId) {
+      return {
+        success: false as const,
+        error: "Unauthorized or no active organization",
+      };
+    }
+
+    const folders = await runPromise(
+      getAllFolders(root).pipe(Effect.provideService(CurrentUser, user))
+    );
+    return { success: true as const, folders };
+  } catch (error) {
+    console.error("Error fetching folders:", error);
+    return {
+      success: false as const,
+      error: "Failed to fetch folders",
+    };
+  }
+}
diff --git a/apps/web/actions/folders/moveVideosToFolder.ts b/apps/web/actions/folders/moveVideosToFolder.ts
new file mode 100644
index 0000000000..0d913becac
--- /dev/null
+++ b/apps/web/actions/folders/moveVideosToFolder.ts
@@ -0,0 +1,84 @@
+"use server";
+
+import { getCurrentUser } from "@cap/database/auth/session";
+import { CurrentUser, Video, Folder } from "@cap/web-domain";
+import { Effect } from "effect";
+import { moveVideosToFolder } from "../../lib/folder";
+import { runPromise } from "../../lib/server";
+import { revalidatePath } from "next/cache";
+
+interface MoveVideosToFolderParams {
+  videoIds: string[];
+  targetFolderId: string | null;
+  spaceId?: string | null;
+}
+
+export async function moveVideosToFolderAction({
+  videoIds,
+  targetFolderId,
+  spaceId,
+}: MoveVideosToFolderParams) {
+  try {
+    const user = await getCurrentUser();
+    if (!user || !user.activeOrganizationId) {
+      return {
+        success: false as const,
+        error: "Unauthorized or no active organization",
+      };
+    }
+
+    const typedVideoIds = videoIds.map((id) => Video.VideoId.make(id));
+    const typedTargetFolderId = targetFolderId
+      ? Folder.FolderId.make(targetFolderId)
+      : null;
+
+    const root = spaceId
+      ? { variant: "space" as const, spaceId }
+      : { variant: "org" as const, organizationId: user.activeOrganizationId };
+
+    const result = await runPromise(
+      moveVideosToFolder(typedVideoIds, typedTargetFolderId, root).pipe(
+        Effect.provideService(CurrentUser, user)
+      )
+    );
+
+    // Revalidate paths
+    revalidatePath("/dashboard/caps");
+
+    if (spaceId) {
+      revalidatePath(`/dashboard/spaces/${spaceId}`);
+      result.originalFolderIds.forEach((folderId) => {
+        if (folderId) {
+          revalidatePath(`/dashboard/spaces/${spaceId}/folder/${folderId}`);
+        }
+      });
+      if (result.targetFolderId) {
+        revalidatePath(
+          `/dashboard/spaces/${spaceId}/folder/${result.targetFolderId}`
+        );
+      }
+    } else {
+      result.originalFolderIds.forEach((folderId) => {
+        if (folderId) {
+          revalidatePath(`/dashboard/folder/${folderId}`);
+        }
+      });
+      if (result.targetFolderId) {
+        revalidatePath(`/dashboard/folder/${result.targetFolderId}`);
+      }
+    }
+
+    return {
+      success: true as const,
+      message: `Successfully moved ${result.movedCount} video${
+        result.movedCount !== 1 ? "s" : ""
+      } to ${result.targetFolderId ? "folder" : "root"}`,
+    };
+  } catch (error) {
+    console.error("Error moving videos to folder:", error);
+    return {
+      success: false as const,
+      error: error instanceof Error ? error.message : "Failed to move videos",
+    };
+  }
+}
diff --git a/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx b/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
new file mode 100644
index 0000000000..e75569147d
--- /dev/null
+++ b/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
@@ -0,0 +1,287 @@
+"use client";
+
+import {
+  Button,
+  Dialog,
+  DialogContent,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@cap/ui";
+import {
+  faFolderOpen,
+  faChevronRight,
+  faChevronDown,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useQuery } from "@tanstack/react-query";
+import { useState } from "react";
+import { getAllFoldersAction } from "../../../../actions/folders/getAllFolders";
+import { moveVideosToFolderAction } from "../../../../actions/folders/moveVideosToFolder";
+import { useDashboardContext } from "../Contexts";
+import { toast } from "sonner";
+
+type FolderWithChildren = {
+  id: string;
+  name: string;
+  color: "normal" | "blue" | "red" | "yellow";
+  parentId: string | null;
+  organizationId: string;
+  videoCount: number;
+  children: FolderWithChildren[];
+};
+
+interface FolderSelectionDialogProps {
+  open: boolean;
+  onClose: () => void;
+  onConfirm: (folderId: string | null) => void;
+  loading?: boolean;
+  selectedCount: number;
+  videoIds: string[];
+}
+
+export function FolderSelectionDialog({
+  open,
+  onClose,
+  onConfirm,
+  loading = false,
+  selectedCount,
+  videoIds,
+}: FolderSelectionDialogProps) {
+  const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
+  const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
+    new Set()
+  );
+  const { activeOrganization, activeSpace } = useDashboardContext();
+
+  // Fetch folders using the server action
+  const { data: foldersData, isLoading } = useQuery({
+    queryKey: ["folders", activeOrganization?.organization.id, activeSpace?.id],
+    queryFn: async () => {
+      const root = activeSpace?.id
+        ? { variant: "space" as const, spaceId: activeSpace.id }
+        : {
+            variant: "org" as const,
+            organizationId: activeOrganization!.organization.id,
+          };
+
+      const result = await getAllFoldersAction(root);
+
+      if (!result.success) {
+        throw new Error(result.error || "Failed to fetch folders");
+      }
+
+      return result.folders;
+    },
+    enabled: open && !!activeOrganization?.organization.id,
+  });
+
+  const folders = foldersData || [];
+
+  // Toggle folder expansion
+  const toggleFolderExpansion = (folderId: string) => {
+    setExpandedFolders((prev) => {
+      const newSet = new Set(prev);
+      if (newSet.has(folderId)) {
+        newSet.delete(folderId);
+      } else {
+        newSet.add(folderId);
+      }
+      return newSet;
+    });
+  };
+
+  // Render folder with improved design
+  const renderFolder = (folder: FolderWithChildren, depth = 0) => {
+    const hasChildren = folder.children && folder.children.length > 0;
+    const isExpanded = expandedFolders.has(folder.id);
+    const isSelected = selectedFolderId === folder.id;
+
+    return (
+      <div key={folder.id} className="relative">
+        <div
+          onClick={() => setSelectedFolderId(folder.id)}
+          className={`
+            group flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer
+            transition-colors border-l-4
+            ${
+              isSelected
+                ? "bg-blue-3 border-blue-9"
+                : "border-transparent hover:bg-gray-3"
+            }
+          `}
+          style={{ marginLeft: `${depth * 16}px` }}
+        >
+          {/* Expand/Collapse button */}
+          {hasChildren ? (
+            <button
+              onClick={(e) => {
+                e.stopPropagation();
+                toggleFolderExpansion(folder.id);
+              }}
+              className="flex-shrink-0 w-5 h-5 flex items-center justify-center rounded hover:bg-gray-4 transition"
+            >
+              <FontAwesomeIcon
+                className="w-3.5 h-3.5 text-gray-10"
+                icon={isExpanded ? faChevronDown : faChevronRight}
+              />
+            </button>
+          ) : (
+            <div className="w-5 h-5 flex-shrink-0" />
+          )}
+
+          {/* Folder icon */}
+          <div className="flex items-center gap-2 flex-1">
+            <div className="flex-shrink-0 w-7 h-7 rounded-md bg-gray-2 border border-gray-6 shadow-sm flex items-center justify-center">
+              <FontAwesomeIcon
+                className="w-4 h-4 text-gray-11"
+                icon={faFolderOpen}
+              />
+            </div>
+
+            <div className="flex-1">
+              <p className="text-sm font-medium text-gray-12">{folder.name}</p>
+              <p className="text-xs text-gray-9">
+                {folder.videoCount} video{folder.videoCount !== 1 ? "s" : ""}
+              </p>
+            </div>
+          </div>
+
+          {/* Selection indicator */}
+          {isSelected && (
+            <div className="ml-auto w-5 h-5 rounded-full bg-blue-9 flex items-center justify-center">
+              <div className="w-2.5 h-2.5 bg-white rounded-full" />
+            </div>
+          )}
+        </div>
+
+        {/* Render children if expanded */}
+        {hasChildren && isExpanded && (
+          <div className="mt-1">
+            {folder.children.map((child) => renderFolder(child, depth + 1))}
+          </div>
+        )}
+      </div>
+    );
+  };
+
+  const handleConfirm = async () => {
+    try {
+      const result = await moveVideosToFolderAction({
+        videoIds,
+        targetFolderId: selectedFolderId,
+        spaceId: activeSpace?.id,
+      });
+
+      if (result.success) {
+        toast.success(result.message);
+        onConfirm(selectedFolderId);
+        setSelectedFolderId(null);
+      } else {
+        toast.error(result.error);
+      }
+    } catch (error) {
+      toast.error("Failed to move videos");
+      console.error("Error moving videos:", error);
+    }
+  };
+
+  const handleCancel = () => {
+    setSelectedFolderId(null);
+    onClose();
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={(open) => !open && handleCancel()}>
+      <DialogContent className="p-0 w-[calc(100%-20px)] max-w-md rounded-xl border bg-gray-2 border-gray-4">
+        <DialogHeader icon={<FontAwesomeIcon icon={faFolderOpen} />}>
+          <DialogTitle className="text-lg text-gray-12">
+            Move {selectedCount} cap{selectedCount !== 1 ? "s" : ""} to folder
+          </DialogTitle>
+        </DialogHeader>
+        <div className="p-5">
+          <p className="mb-4 text-[14px] leading-5 text-gray-11">
+            Select a destination folder for the selected caps.
+          </p>
+
+          <div className="space-y-2 max-h-[320px] overflow-y-auto custom-scroll">
+            {/* Option to move to root (no folder) */}
+            <div
+              onClick={() => setSelectedFolderId(null)}
+              className={`
+                flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer
+                transition-colors border-l-4
+                ${
+                  selectedFolderId === null
+                    ? "bg-blue-3 border-blue-9"
+                    : "border-transparent hover:bg-gray-3"
+                }
+              `}
+            >
+              <div className="flex-shrink-0 w-7 h-7 rounded-md bg-gray-2 border border-gray-6 shadow-sm flex items-center justify-center">
+                <FontAwesomeIcon
+                  className="w-4 h-4 text-gray-11"
+                  icon={faFolderOpen}
+                />
+              </div>
+              <div>
+                <p className="text-sm font-medium text-gray-12">
+                  {activeOrganization?.organization.name || "My Caps"}
+                </p>
+                <p className="text-xs text-gray-9">Move to root folder</p>
+              </div>
+              {selectedFolderId === null && (
+                <div className="ml-auto w-5 h-5 rounded-full bg-blue-9 flex items-center justify-center">
+                  <div className="w-2.5 h-2.5 bg-white rounded-full" />
+                </div>
+              )}
+            </div>
+
+            {/* Loading state */}
+            {isLoading && (
+              <div className="text-center py-4">
+                <p className="text-sm text-gray-10">Loading folders...</p>
+              </div>
+            )}
+
+            {/* Available folders with hierarchy */}
+            {!isLoading &&
+              folders.map((folder: FolderWithChildren) => renderFolder(folder))}
+          </div>
+
+          {!isLoading && folders.length === 0 && (
+            <div className="text-center py-8">
+              <FontAwesomeIcon
+                className="w-8 h-8 text-gray-8 mb-3"
+                icon={faFolderOpen}
+              />
+              <p className="text-sm text-gray-10">No folders available</p>
+              <p className="text-xs text-gray-9 mt-1">
+                Create a folder first to organize your caps
+              </p>
+            </div>
+          )}
+        </div>
+        <DialogFooter>
+          <Button
+            onClick={handleCancel}
+            variant="gray"
+            size="sm"
+            disabled={loading}
+          >
+            Cancel
+          </Button>
+          <Button
+            onClick={handleConfirm}
+            variant="dark"
+            size="sm"
+            spinner={loading}
+            disabled={loading}
+          >
+            {loading ? "Moving..." : "Move"}
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}
diff --git a/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx b/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
index 8f82e8c6d7..6ccf19909b 100644
--- a/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
+++ b/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
@@ -1,98 +1,131 @@
 "use client";
 
 import { Button } from "@cap/ui";
-import { faFilm, faTrash } from "@fortawesome/free-solid-svg-icons";
+import {
+  faFilm,
+  faTrash,
+  faFolderOpen,
+} from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import NumberFlow from "@number-flow/react";
 import { AnimatePresence, motion } from "framer-motion";
 import { ConfirmationDialog } from "@/app/(org)/dashboard/_components/ConfirmationDialog";
+import { FolderSelectionDialog } from "@/app/(org)/dashboard/_components/FolderSelectionDialog";
 
 interface SelectedCapsBarProps {
-	selectedCaps: string[];
-	setSelectedCaps: (caps: Video.VideoId[]) => void;
-	deleteSelectedCaps: () => void;
-	isDeleting: boolean;
+  selectedCaps: string[];
+  setSelectedCaps: (caps: Video.VideoId[]) => void;
+  deleteSelectedCaps: () => void;
+  isDeleting: boolean;
+  moveSelectedCaps?: (folderId: string | null) => void;
 }
 
 import type { Video } from "@cap/web-domain";
 import { useState } from "react";
 
 export const SelectedCapsBar = ({
-	selectedCaps,
-	setSelectedCaps,
-	deleteSelectedCaps,
-	isDeleting,
+  selectedCaps,
+  setSelectedCaps,
+  deleteSelectedCaps,
+  isDeleting,
+  moveSelectedCaps,
 }: SelectedCapsBarProps) => {
-	const [confirmOpen, setConfirmOpen] = useState(false);
+  const [confirmOpen, setConfirmOpen] = useState(false);
+  const [moveDialogOpen, setMoveDialogOpen] = useState(false);
 
-	const handleConfirmDelete = () => {
-		deleteSelectedCaps();
-		setConfirmOpen(false);
-	};
+  const handleConfirmDelete = () => {
+    deleteSelectedCaps();
+    setConfirmOpen(false);
+  };
 
-	return (
-		<AnimatePresence>
-			{selectedCaps.length > 0 && (
-				<motion.div
-					className="flex fixed right-0 left-0 bottom-12 z-50 justify-between items-center p-3 mx-auto w-full max-w-xl rounded-xl border shadow-lg border-gray-2 bg-gray-1"
-					initial={{ opacity: 0, y: 10, scale: 0.9 }}
-					animate={{ opacity: 1, y: 0, scale: 1 }}
-					exit={{
-						opacity: 0,
-						y: 10,
-						scale: 0.9,
-						transition: { duration: 0.2 },
-					}}
-					transition={{
-						opacity: { duration: 0.2, ease: "easeOut" },
-						y: { type: "spring", damping: 15, stiffness: 300 },
-						scale: { type: "spring", damping: 15, stiffness: 300 },
-					}}
-				>
-					<div className="flex gap-1 text-sm font-medium text-gray-10">
-						<NumberFlow
-							value={selectedCaps.length}
-							className="tabular-nums text-md text-gray-12"
-						/>
-						cap{selectedCaps.length !== 1 ? "s" : ""} selected
-					</div>
-					<div className="flex gap-2 ml-4">
-						<Button
-							variant="dark"
-							onClick={() => setSelectedCaps([])}
-							className="text-sm"
-							size="sm"
-						>
-							Cancel
-						</Button>
-						<Button
-							variant="destructive"
-							onClick={() => setConfirmOpen(true)}
-							disabled={isDeleting}
-							className="size-[40px] min-w-[unset] p-0"
-							size="sm"
-						>
-							<FontAwesomeIcon className="w-3.5 text-white" icon={faTrash} />
-						</Button>
-						<ConfirmationDialog
-							open={confirmOpen}
-							icon={<FontAwesomeIcon icon={faFilm} />}
-							title="Delete selected Caps"
-							description={`Are you sure you want to delete ${
-								selectedCaps.length
-							} cap${
-								selectedCaps.length === 1 ? "" : "s"
-							}? This action cannot be undone.`}
-							confirmLabel={isDeleting ? "Deleting..." : "Delete"}
-							cancelLabel="Cancel"
-							confirmVariant="dark"
-							loading={isDeleting}
-							onConfirm={handleConfirmDelete}
-							onCancel={() => setConfirmOpen(false)}
-						/>
-					</div>
-				</motion.div>
-			)}
-		</AnimatePresence>
-	);
+  const handleMoveToFolder = (folderId: string | null) => {
+    if (moveSelectedCaps) {
+      moveSelectedCaps(folderId);
+    }
+    setMoveDialogOpen(false);
+  };
+
+  return (
+    <AnimatePresence>
+      {selectedCaps.length > 0 && (
+        <motion.div
+          className="flex fixed right-0 left-0 bottom-12 z-50 justify-between items-center p-3 mx-auto w-full max-w-xl rounded-xl border shadow-lg border-gray-2 bg-gray-1"
+          initial={{ opacity: 0, y: 10, scale: 0.9 }}
+          animate={{ opacity: 1, y: 0, scale: 1 }}
+          exit={{
+            opacity: 0,
+            y: 10,
+            scale: 0.9,
+            transition: { duration: 0.2 },
+          }}
+          transition={{
+            opacity: { duration: 0.2, ease: "easeOut" },
+            y: { type: "spring", damping: 15, stiffness: 300 },
+            scale: { type: "spring", damping: 15, stiffness: 300 },
+          }}
+        >
+          <div className="flex gap-1 text-sm font-medium text-gray-10">
+            <NumberFlow
+              value={selectedCaps.length}
+              className="tabular-nums text-md text-gray-12"
+            />
+            cap{selectedCaps.length !== 1 ? "s" : ""} selected
+          </div>
+          <div className="flex gap-2 ml-4">
+            <Button
+              variant="dark"
+              onClick={() => setSelectedCaps([])}
+              className="text-sm"
+              size="sm"
+            >
+              Cancel
+            </Button>
+            <Button
+              variant="gray"
+              onClick={() => setMoveDialogOpen(true)}
+              disabled={isDeleting}
+              className="size-[40px] min-w-[unset] p-0"
+              size="sm"
+            >
+              <FontAwesomeIcon className="w-3.5" icon={faFolderOpen} />
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={() => setConfirmOpen(true)}
+              disabled={isDeleting}
+              className="size-[40px] min-w-[unset] p-0"
+              size="sm"
+            >
+              <FontAwesomeIcon className="w-3.5 text-white" icon={faTrash} />
+            </Button>
+            <ConfirmationDialog
+              open={confirmOpen}
+              icon={<FontAwesomeIcon icon={faFilm} />}
+              title="Delete selected Caps"
+              description={`Are you sure you want to delete ${
+                selectedCaps.length
+              } cap${
+                selectedCaps.length === 1 ? "" : "s"
+              }? This action cannot be undone.`}
+              confirmLabel={isDeleting ? "Deleting..." : "Delete"}
+              cancelLabel="Cancel"
+              confirmVariant="dark"
+              loading={isDeleting}
+              onConfirm={handleConfirmDelete}
+              onCancel={() => setConfirmOpen(false)}
+            />
+
+            <FolderSelectionDialog
+              open={moveDialogOpen}
+              onClose={() => setMoveDialogOpen(false)}
+              onConfirm={handleMoveToFolder}
+              selectedCount={selectedCaps.length}
+              loading={false}
+              videoIds={selectedCaps}
+            />
+          </div>
+        </motion.div>
+      )}
+    </AnimatePresence>
+  );
 };
diff --git a/apps/web/lib/folder.ts b/apps/web/lib/folder.ts
index ef6d7c260e..530c73d281 100644
--- a/apps/web/lib/folder.ts
+++ b/apps/web/lib/folder.ts
@@ -3,181 +3,181 @@ import "server-only";
 import { db } from "@cap/database";
 import { getCurrentUser } from "@cap/database/auth/session";
 import {
-	comments,
-	folders,
-	organizations,
-	sharedVideos,
-	spaces,
-	spaceVideos,
-	users,
-	videos,
-	videoUploads,
+  comments,
+  folders,
+  organizations,
+  sharedVideos,
+  spaces,
+  spaceVideos,
+  users,
+  videos,
+  videoUploads,
 } from "@cap/database/schema";
 import { Database } from "@cap/web-backend";
 import type { Video } from "@cap/web-domain";
 import { CurrentUser, Folder } from "@cap/web-domain";
-import { and, desc, eq } from "drizzle-orm";
+import { and, desc, eq, inArray } from "drizzle-orm";
 import { sql } from "drizzle-orm/sql";
 import { Effect } from "effect";
 import { revalidatePath } from "next/cache";
 
 export const getFolderById = Effect.fn(function* (folderId: string) {
-	if (!folderId) throw new Error("Folder ID is required");
-	const db = yield* Database;
+  if (!folderId) throw new Error("Folder ID is required");
+  const db = yield* Database;
 
-	const [folder] = yield* db.execute((db) =>
-		db
-			.select()
-			.from(folders)
-			.where(eq(folders.id, Folder.FolderId.make(folderId))),
-	);
+  const [folder] = yield* db.execute((db) =>
+    db
+      .select()
+      .from(folders)
+      .where(eq(folders.id, Folder.FolderId.make(folderId)))
+  );
 
-	if (!folder) throw new Error("Folder not found");
+  if (!folder) throw new Error("Folder not found");
 
-	return folder;
+  return folder;
 });
 
 export const getFolderBreadcrumb = Effect.fn(function* (
-	folderId: Folder.FolderId,
+  folderId: Folder.FolderId
 ) {
-	const breadcrumb: Array<{
-		id: Folder.FolderId;
-		name: string;
-		color: "normal" | "blue" | "red" | "yellow";
-	}> = [];
-	let currentFolderId = folderId;
-
-	while (currentFolderId) {
-		const folder = yield* getFolderById(currentFolderId);
-		if (!folder) break;
-
-		breadcrumb.unshift({
-			id: folder.id,
-			name: folder.name,
-			color: folder.color,
-		});
-
-		if (!folder.parentId) break;
-		currentFolderId = folder.parentId;
-	}
-
-	return breadcrumb;
+  const breadcrumb: Array<{
+    id: Folder.FolderId;
+    name: string;
+    color: "normal" | "blue" | "red" | "yellow";
+  }> = [];
+  let currentFolderId = folderId;
+
+  while (currentFolderId) {
+    const folder = yield* getFolderById(currentFolderId);
+    if (!folder) break;
+
+    breadcrumb.unshift({
+      id: folder.id,
+      name: folder.name,
+      color: folder.color,
+    });
+
+    if (!folder.parentId) break;
+    currentFolderId = folder.parentId;
+  }
+
+  return breadcrumb;
 });
 
 // Helper function to fetch shared spaces data for videos
 const getSharedSpacesForVideos = Effect.fn(function* (
-	videoIds: Video.VideoId[],
+  videoIds: Video.VideoId[]
 ) {
-	if (videoIds.length === 0) return {};
-	const db = yield* Database;
-
-	// Fetch space-level sharing
-	const spaceSharing = yield* db.execute((db) =>
-		db
-			.select({
-				videoId: spaceVideos.videoId,
-				id: spaces.id,
-				name: spaces.name,
-				organizationId: spaces.organizationId,
-				iconUrl: organizations.iconUrl,
-			})
-			.from(spaceVideos)
-			.innerJoin(spaces, eq(spaceVideos.spaceId, spaces.id))
-			.innerJoin(organizations, eq(spaces.organizationId, organizations.id))
-			.where(
-				sql`${spaceVideos.videoId} IN (${sql.join(
-					videoIds.map((id) => sql`${id}`),
-					sql`, `,
-				)})`,
-			),
-	);
-
-	// Fetch organization-level sharing
-	const orgSharing = yield* db.execute((db) =>
-		db
-			.select({
-				videoId: sharedVideos.videoId,
-				id: organizations.id,
-				name: organizations.name,
-				organizationId: organizations.id,
-				iconUrl: organizations.iconUrl,
-			})
-			.from(sharedVideos)
-			.innerJoin(
-				organizations,
-				eq(sharedVideos.organizationId, organizations.id),
-			)
-			.where(
-				sql`${sharedVideos.videoId} IN (${sql.join(
-					videoIds.map((id) => sql`${id}`),
-					sql`, `,
-				)})`,
-			),
-	);
-
-	// Combine and group by videoId
-	const sharedSpacesMap: Record<
-		string,
-		Array<{
-			id: string;
-			name: string;
-			organizationId: string;
-			iconUrl: string;
-			isOrg: boolean;
-		}>
-	> = {};
-
-	// Add space-level sharing
-	spaceSharing.forEach((space) => {
-		const spaces = sharedSpacesMap[space.videoId] ?? [];
-		sharedSpacesMap[space.videoId] = spaces;
-		spaces.push({
-			id: space.id,
-			name: space.name,
-			organizationId: space.organizationId,
-			iconUrl: space.iconUrl || "",
-			isOrg: false,
-		});
-	});
-
-	// Add organization-level sharing
-	orgSharing.forEach((org) => {
-		const spaces = sharedSpacesMap[org.videoId] ?? [];
-		sharedSpacesMap[org.videoId] = spaces;
-
-		spaces.push({
-			id: org.id,
-			name: org.name,
-			organizationId: org.organizationId,
-			iconUrl: org.iconUrl || "",
-			isOrg: true,
-		});
-	});
-
-	return sharedSpacesMap;
+  if (videoIds.length === 0) return {};
+  const db = yield* Database;
+
+  // Fetch space-level sharing
+  const spaceSharing = yield* db.execute((db) =>
+    db
+      .select({
+        videoId: spaceVideos.videoId,
+        id: spaces.id,
+        name: spaces.name,
+        organizationId: spaces.organizationId,
+        iconUrl: organizations.iconUrl,
+      })
+      .from(spaceVideos)
+      .innerJoin(spaces, eq(spaceVideos.spaceId, spaces.id))
+      .innerJoin(organizations, eq(spaces.organizationId, organizations.id))
+      .where(
+        sql`${spaceVideos.videoId} IN (${sql.join(
+          videoIds.map((id) => sql`${id}`),
+          sql`, `
+        )})`
+      )
+  );
+
+  // Fetch organization-level sharing
+  const orgSharing = yield* db.execute((db) =>
+    db
+      .select({
+        videoId: sharedVideos.videoId,
+        id: organizations.id,
+        name: organizations.name,
+        organizationId: organizations.id,
+        iconUrl: organizations.iconUrl,
+      })
+      .from(sharedVideos)
+      .innerJoin(
+        organizations,
+        eq(sharedVideos.organizationId, organizations.id)
+      )
+      .where(
+        sql`${sharedVideos.videoId} IN (${sql.join(
+          videoIds.map((id) => sql`${id}`),
+          sql`, `
+        )})`
+      )
+  );
+
+  // Combine and group by videoId
+  const sharedSpacesMap: Record<
+    string,
+    Array<{
+      id: string;
+      name: string;
+      organizationId: string;
+      iconUrl: string;
+      isOrg: boolean;
+    }>
+  > = {};
+
+  // Add space-level sharing
+  spaceSharing.forEach((space) => {
+    const spaces = sharedSpacesMap[space.videoId] ?? [];
+    sharedSpacesMap[space.videoId] = spaces;
+    spaces.push({
+      id: space.id,
+      name: space.name,
+      organizationId: space.organizationId,
+      iconUrl: space.iconUrl || "",
+      isOrg: false,
+    });
+  });
+
+  // Add organization-level sharing
+  orgSharing.forEach((org) => {
+    const spaces = sharedSpacesMap[org.videoId] ?? [];
+    sharedSpacesMap[org.videoId] = spaces;
+
+    spaces.push({
+      id: org.id,
+      name: org.name,
+      organizationId: org.organizationId,
+      iconUrl: org.iconUrl || "",
+      isOrg: true,
+    });
+  });
+
+  return sharedSpacesMap;
 });
 
 export const getVideosByFolderId = Effect.fn(function* (
-	folderId: Folder.FolderId,
+  folderId: Folder.FolderId
 ) {
-	if (!folderId) throw new Error("Folder ID is required");
-	const db = yield* Database;
-
-	const videoData = yield* db.execute((db) =>
-		db
-			.select({
-				id: videos.id,
-				ownerId: videos.ownerId,
-				name: videos.name,
-				createdAt: videos.createdAt,
-				public: videos.public,
-				metadata: videos.metadata,
-				duration: videos.duration,
-				totalComments: sql<number>`COUNT(DISTINCT CASE WHEN ${comments.type} = 'text' THEN ${comments.id} END)`,
-				totalReactions: sql<number>`COUNT(DISTINCT CASE WHEN ${comments.type} = 'emoji' THEN ${comments.id} END)`,
-				sharedOrganizations: sql<
-					{ id: string; name: string; iconUrl: string }[]
-				>`
+  if (!folderId) throw new Error("Folder ID is required");
+  const db = yield* Database;
+
+  const videoData = yield* db.execute((db) =>
+    db
+      .select({
+        id: videos.id,
+        ownerId: videos.ownerId,
+        name: videos.name,
+        createdAt: videos.createdAt,
+        public: videos.public,
+        metadata: videos.metadata,
+        duration: videos.duration,
+        totalComments: sql<number>`COUNT(DISTINCT CASE WHEN ${comments.type} = 'text' THEN ${comments.id} END)`,
+        totalReactions: sql<number>`COUNT(DISTINCT CASE WHEN ${comments.type} = 'emoji' THEN ${comments.id} END)`,
+        sharedOrganizations: sql<
+          { id: string; name: string; iconUrl: string }[]
+        >`
         COALESCE(
           JSON_ARRAYAGG(
             JSON_OBJECT(
@@ -190,117 +190,260 @@ export const getVideosByFolderId = Effect.fn(function* (
         )
       `,
 
-				ownerName: users.name,
-				effectiveDate: sql<string>`
+        ownerName: users.name,
+        effectiveDate: sql<string>`
         COALESCE(
           JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')),
           ${videos.createdAt}
         )
       `,
-				hasPassword: sql`${videos.password} IS NOT NULL`.mapWith(Boolean),
-				hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith(
-					Boolean,
-				),
-			})
-			.from(videos)
-			.leftJoin(comments, eq(videos.id, comments.videoId))
-			.leftJoin(sharedVideos, eq(videos.id, sharedVideos.videoId))
-			.leftJoin(
-				organizations,
-				eq(sharedVideos.organizationId, organizations.id),
-			)
-			.leftJoin(users, eq(videos.ownerId, users.id))
-			.leftJoin(videoUploads, eq(videos.id, videoUploads.videoId))
-			.where(eq(videos.folderId, folderId))
-			.groupBy(
-				videos.id,
-				videos.ownerId,
-				videos.name,
-				videos.createdAt,
-				videos.public,
-				videos.metadata,
-				users.name,
-			)
-			.orderBy(
-				desc(sql`COALESCE(
+        hasPassword: sql`${videos.password} IS NOT NULL`.mapWith(Boolean),
+        hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith(
+          Boolean
+        ),
+      })
+      .from(videos)
+      .leftJoin(comments, eq(videos.id, comments.videoId))
+      .leftJoin(sharedVideos, eq(videos.id, sharedVideos.videoId))
+      .leftJoin(
+        organizations,
+        eq(sharedVideos.organizationId, organizations.id)
+      )
+      .leftJoin(users, eq(videos.ownerId, users.id))
+      .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId))
+      .where(eq(videos.folderId, folderId))
+      .groupBy(
+        videos.id,
+        videos.ownerId,
+        videos.name,
+        videos.createdAt,
+        videos.public,
+        videos.metadata,
+        users.name
+      )
+      .orderBy(
+        desc(sql`COALESCE(
       JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')),
       ${videos.createdAt}
-    )`),
-			),
-	);
-
-	// Fetch shared spaces data for all videos
-	const videoIds = videoData.map((video) => video.id);
-	const sharedSpacesMap = yield* getSharedSpacesForVideos(videoIds);
-
-	// Process the video data to match the expected format
-	const processedVideoData = videoData.map((video) => {
-		return {
-			id: video.id as Video.VideoId, // Cast to Video.VideoId branded type
-			ownerId: video.ownerId,
-			name: video.name,
-			createdAt: video.createdAt,
-			public: video.public,
-			totalComments: video.totalComments,
-			totalReactions: video.totalReactions,
-			sharedOrganizations: Array.isArray(video.sharedOrganizations)
-				? video.sharedOrganizations.filter(
-						(organization) => organization.id !== null,
-					)
-				: [],
-			sharedSpaces: Array.isArray(sharedSpacesMap[video.id])
-				? sharedSpacesMap[video.id]
-				: [],
-			ownerName: video.ownerName ?? "",
-			metadata: video.metadata as
-				| {
-						customCreatedAt?: string;
-						[key: string]: unknown;
-				  }
-				| undefined,
-			hasPassword: video.hasPassword,
-			hasActiveUpload: video.hasActiveUpload,
-			foldersData: [], // Empty array since videos in a folder don't need folder data
-		};
-	});
-
-	return processedVideoData;
+    )`)
+      )
+  );
+
+  // Fetch shared spaces data for all videos
+  const videoIds = videoData.map((video) => video.id);
+  const sharedSpacesMap = yield* getSharedSpacesForVideos(videoIds);
+
+  // Process the video data to match the expected format
+  const processedVideoData = videoData.map((video) => {
+    return {
+      id: video.id as Video.VideoId, // Cast to Video.VideoId branded type
+      ownerId: video.ownerId,
+      name: video.name,
+      createdAt: video.createdAt,
+      public: video.public,
+      totalComments: video.totalComments,
+      totalReactions: video.totalReactions,
+      sharedOrganizations: Array.isArray(video.sharedOrganizations)
+        ? video.sharedOrganizations.filter(
+            (organization) => organization.id !== null
+          )
+        : [],
+      sharedSpaces: Array.isArray(sharedSpacesMap[video.id])
+        ? sharedSpacesMap[video.id]
+        : [],
+      ownerName: video.ownerName ?? "",
+      metadata: video.metadata as
+        | {
+            customCreatedAt?: string;
+            [key: string]: unknown;
+          }
+        | undefined,
+      hasPassword: video.hasPassword,
+      hasActiveUpload: video.hasActiveUpload,
+      foldersData: [], // Empty array since videos in a folder don't need folder data
+    };
+  });
+
+  return processedVideoData;
 });
 
 export const getChildFolders = Effect.fn(function* (
-	folderId: Folder.FolderId,
-	root:
-		| { variant: "user" }
-		| { variant: "space"; spaceId: string }
-		| { variant: "org"; organizationId: string },
+  folderId: Folder.FolderId,
+  root:
+    | { variant: "user" }
+    | { variant: "space"; spaceId: string }
+    | { variant: "org"; organizationId: string }
 ) {
-	const db = yield* Database;
-
-	const user = yield* CurrentUser;
-	if (!user.activeOrganizationId) throw new Error("No active organization");
-
-	const childFolders = yield* db.execute((db) =>
-		db
-			.select({
-				id: folders.id,
-				name: folders.name,
-				color: folders.color,
-				parentId: folders.parentId,
-				organizationId: folders.organizationId,
-				videoCount: sql<number>`(
+  const db = yield* Database;
+
+  const user = yield* CurrentUser;
+  if (!user.activeOrganizationId) throw new Error("No active organization");
+
+  const childFolders = yield* db.execute((db) =>
+    db
+      .select({
+        id: folders.id,
+        name: folders.name,
+        color: folders.color,
+        parentId: folders.parentId,
+        organizationId: folders.organizationId,
+        videoCount: sql<number>`(
         	SELECT COUNT(*) FROM videos WHERE videos.folderId = folders.id
 	      )`,
-			})
-			.from(folders)
-			.where(
-				and(
-					eq(folders.parentId, folderId),
-					root.variant === "space"
-						? eq(folders.spaceId, root.spaceId)
-						: undefined,
-				),
-			),
-	);
-
-	return childFolders;
+      })
+      .from(folders)
+      .where(
+        and(
+          eq(folders.parentId, folderId),
+          root.variant === "space"
+            ? eq(folders.spaceId, root.spaceId)
+            : undefined
+        )
+      )
+  );
+
+  return childFolders;
+});
+
+export const getAllFolders = Effect.fn(function* (
+  root:
+    | { variant: "user" }
+    | { variant: "space"; spaceId: string }
+    | { variant: "org"; organizationId: string }
+) {
+  const db = yield* Database;
+  const user = yield* CurrentUser;
+
+  if (!user.activeOrganizationId) throw new Error("No active organization");
+
+  // Get all folders in one query
+  const allFolders = yield* db.execute((db) =>
+    db
+      .select({
+        id: folders.id,
+        name: folders.name,
+        color: folders.color,
+        parentId: folders.parentId,
+        organizationId: folders.organizationId,
+        videoCount: sql<number>`(
+					SELECT COUNT(*) FROM videos WHERE videos.folderId = folders.id
+				)`,
+      })
+      .from(folders)
+      .where(
+        and(
+          eq(folders.organizationId, user.activeOrganizationId),
+          root.variant === "space"
+            ? eq(folders.spaceId, root.spaceId)
+            : undefined
+        )
+      )
+  );
+
+  // Define the folder with children type
+  type FolderWithChildren = {
+    id: string;
+    name: string;
+    color: "normal" | "blue" | "red" | "yellow";
+    parentId: string | null;
+    organizationId: string;
+    videoCount: number;
+    children: FolderWithChildren[];
+  };
+
+  // Build hierarchy client-side
+  const folderMap = new Map<string, FolderWithChildren>();
+  const rootFolders: FolderWithChildren[] = [];
+
+  // First pass: create folder objects with children array
+  allFolders.forEach((folder) => {
+    folderMap.set(folder.id, { ...folder, children: [] });
+  });
+
+  // Second pass: build parent-child relationships
+  allFolders.forEach((folder) => {
+    const folderWithChildren = folderMap.get(folder.id);
+
+    if (folder.parentId) {
+      const parent = folderMap.get(folder.parentId);
+      if (parent && folderWithChildren) {
+        parent.children.push(folderWithChildren);
+      }
+    } else {
+      if (folderWithChildren) {
+        rootFolders.push(folderWithChildren);
+      }
+    }
+  });
+
+  return rootFolders;
+});
+
+export const moveVideosToFolder = Effect.fn(function* (
+  videoIds: Video.VideoId[],
+  targetFolderId: Folder.FolderId | null,
+  root?:
+    | { variant: "space"; spaceId: string }
+    | { variant: "org"; organizationId: string }
+) {
+  if (videoIds.length === 0) throw new Error("No videos to move");
+
+  const db = yield* Database;
+  const user = yield* CurrentUser;
+
+  if (!user.activeOrganizationId) throw new Error("No active organization");
+
+  // Validate that all videos exist and belong to the user
+  const existingVideos = yield* db.execute((db) =>
+    db
+      .select({
+        id: videos.id,
+        folderId: videos.folderId,
+        ownerId: videos.ownerId,
+      })
+      .from(videos)
+      .where(and(inArray(videos.id, videoIds), eq(videos.ownerId, user.id)))
+  );
+
+  if (existingVideos.length !== videoIds.length) {
+    throw new Error(
+      "Some videos not found or you don't have permission to move them"
+    );
+  }
+
+  // If target folder is specified, validate it exists and user has access
+  if (targetFolderId) {
+    const targetFolder = yield* getFolderById(targetFolderId);
+
+    if (targetFolder.organizationId !== user.activeOrganizationId) {
+      throw new Error("Target folder not found or you don't have access to it");
+    }
+
+    // Validate space context if provided
+    if (root?.variant === "space" && targetFolder.spaceId !== root.spaceId) {
+      throw new Error("Target folder does not belong to the specified space");
+    }
+  }
+
+  // Store original folder IDs for potential revalidation
+  const originalFolderIds = [
+    ...new Set(existingVideos.map((v) => v.folderId).filter(Boolean)),
+  ];
+
+  // Perform the move operation
+  yield* db.execute((db) =>
+    db
+      .update(videos)
+      .set({
+        folderId: targetFolderId,
+        updatedAt: new Date(),
+      })
+      .where(inArray(videos.id, videoIds))
+  );
+
+  return {
+    movedCount: videoIds.length,
+    originalFolderIds,
+    targetFolderId,
+  };
 });

From 91415b3243c93a1efb586f0692928ebdc3e46a6f Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Fri, 3 Oct 2025 16:23:01 +0100
Subject: [PATCH 2/9] fix: resolve server action build errors and add space
 video support

---
 apps/web/lib/folder.ts | 67 ++++++++++++++++++++++++++++++++----------
 1 file changed, 52 insertions(+), 15 deletions(-)

diff --git a/apps/web/lib/folder.ts b/apps/web/lib/folder.ts
index 530c73d281..02ccd91e5b 100644
--- a/apps/web/lib/folder.ts
+++ b/apps/web/lib/folder.ts
@@ -295,6 +295,7 @@ export const getChildFolders = Effect.fn(function* (
       .where(
         and(
           eq(folders.parentId, folderId),
+          eq(folders.organizationId, user.activeOrganizationId),
           root.variant === "space"
             ? eq(folders.spaceId, root.spaceId)
             : undefined
@@ -425,21 +426,57 @@ export const moveVideosToFolder = Effect.fn(function* (
     }
   }
 
-  // Store original folder IDs for potential revalidation
-  const originalFolderIds = [
-    ...new Set(existingVideos.map((v) => v.folderId).filter(Boolean)),
-  ];
-
-  // Perform the move operation
-  yield* db.execute((db) =>
-    db
-      .update(videos)
-      .set({
-        folderId: targetFolderId,
-        updatedAt: new Date(),
-      })
-      .where(inArray(videos.id, videoIds))
-  );
+  // Determine original folder ids and perform the move based on context
+  let originalFolderIds: (string | null)[] = [];
+
+  if (root?.variant === "space") {
+    // Collect originals from space_videos
+    const spaceRows = yield* db.execute((db) =>
+      db
+        .select({
+          folderId: spaceVideos.folderId,
+          videoId: spaceVideos.videoId,
+        })
+        .from(spaceVideos)
+        .where(
+          and(
+            eq(spaceVideos.spaceId, root.spaceId),
+            inArray(spaceVideos.videoId, videoIds)
+          )
+        )
+    );
+    originalFolderIds = [
+      ...new Set(spaceRows.map((r) => r.folderId).filter(Boolean)),
+    ];
+
+    // Update per-space folder placement
+    yield* db.execute((db) =>
+      db
+        .update(spaceVideos)
+        .set({ folderId: targetFolderId })
+        .where(
+          and(
+            eq(spaceVideos.spaceId, root.spaceId),
+            inArray(spaceVideos.videoId, videoIds)
+          )
+        )
+    );
+  } else {
+    // ORG/global placement via videos.folderId
+    originalFolderIds = [
+      ...new Set(existingVideos.map((v) => v.folderId).filter(Boolean)),
+    ];
+
+    yield* db.execute((db) =>
+      db
+        .update(videos)
+        .set({
+          folderId: targetFolderId,
+          updatedAt: new Date(),
+        })
+        .where(inArray(videos.id, videoIds))
+    );
+  }
 
   return {
     movedCount: videoIds.length,

From a15335ad6fda6d3c37ea37e95038c0eabeaee829 Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Fri, 3 Oct 2025 19:32:04 +0100
Subject: [PATCH 3/9] fix: guard against partial space updates in
 moveVideosToFolder

- Add validation to ensure all requested videos exist in the space before updating
- Prevent silent partial updates when some videos are missing from space_videos
- Throw descriptive error when videos are not found in the specified space
- Improves data integrity and prevents misleading success responses
---
 apps/web/lib/folder.ts | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/apps/web/lib/folder.ts b/apps/web/lib/folder.ts
index 02ccd91e5b..2d32ced62d 100644
--- a/apps/web/lib/folder.ts
+++ b/apps/web/lib/folder.ts
@@ -445,6 +445,13 @@ export const moveVideosToFolder = Effect.fn(function* (
           )
         )
     );
+    const spaceVideoIds = new Set(spaceRows.map((row) => row.videoId));
+    const missingVideoIds = videoIds.filter((id) => !spaceVideoIds.has(id));
+    if (missingVideoIds.length > 0) {
+      throw new Error(
+        "Some videos are not in the specified space or you don't have permission to move them"
+      );
+    }
     originalFolderIds = [
       ...new Set(spaceRows.map((r) => r.folderId).filter(Boolean)),
     ];

From 5d8d23c230a008e15bdc5dd23ccddc37c23b172e Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Fri, 3 Oct 2025 19:51:43 +0100
Subject: [PATCH 4/9] fix: add space permission checks to moveVideosToFolder
 action

---
 .../web/actions/folders/moveVideosToFolder.ts | 27 ++++++++++++++-----
 1 file changed, 20 insertions(+), 7 deletions(-)

diff --git a/apps/web/actions/folders/moveVideosToFolder.ts b/apps/web/actions/folders/moveVideosToFolder.ts
index 0d913becac..7570deb276 100644
--- a/apps/web/actions/folders/moveVideosToFolder.ts
+++ b/apps/web/actions/folders/moveVideosToFolder.ts
@@ -1,9 +1,10 @@
 "use server";
 
 import { getCurrentUser } from "@cap/database/auth/session";
-import { CurrentUser, Video, Folder } from "@cap/web-domain";
+import { CurrentUser, Video, Folder, Policy } from "@cap/web-domain";
+import { SpacesPolicy } from "@cap/web-backend";
 import { Effect } from "effect";
-import { moveVideosToFolder } from "../../lib/folder";
+import { moveVideosToFolder, getFolderById } from "../../lib/folder";
 import { runPromise } from "../../lib/server";
 import { revalidatePath } from "next/cache";
 
@@ -36,11 +37,23 @@ export async function moveVideosToFolderAction({
       ? { variant: "space" as const, spaceId }
       : { variant: "org" as const, organizationId: user.activeOrganizationId };
 
-    const result = await runPromise(
-      moveVideosToFolder(typedVideoIds, typedTargetFolderId, root).pipe(
-        Effect.provideService(CurrentUser, user)
-      )
-    );
+    // Create effect with permission checks
+    const moveVideosEffect = spaceId
+      ? Effect.gen(function* () {
+          const spacesPolicy = yield* SpacesPolicy;
+
+          // Perform the folder move operation
+          return yield* moveVideosToFolder(
+            typedVideoIds,
+            typedTargetFolderId,
+            root
+          ).pipe(Policy.withPolicy(spacesPolicy.isMember(spaceId)));
+        }).pipe(Effect.provideService(CurrentUser, user))
+      : moveVideosToFolder(typedVideoIds, typedTargetFolderId, root).pipe(
+          Effect.provideService(CurrentUser, user)
+        );
+
+    const result = await runPromise(moveVideosEffect);
 
     // Revalidate paths
     revalidatePath("/dashboard/caps");

From ea8994c69b353502fe7fd05dfabad71122b07416 Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Sat, 4 Oct 2025 13:58:10 +0100
Subject: [PATCH 5/9] fix: implement folder scope isolation and EffectRuntime
 migration

---
 .../_components/FolderSelectionDialog.tsx     | 110 +++++++++++-------
 .../caps/components/SelectedCapsBar.tsx       |   1 -
 apps/web/lib/folder.ts                        |  13 ++-
 3 files changed, 77 insertions(+), 47 deletions(-)

diff --git a/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx b/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
index e75569147d..c42154faa0 100644
--- a/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
+++ b/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
@@ -14,12 +14,15 @@ import {
   faChevronDown,
 } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useQuery } from "@tanstack/react-query";
+import { useEffectMutation, useEffectQuery } from "@/lib/EffectRuntime";
+import { withRpc } from "@/lib/Rpcs";
 import { useState } from "react";
 import { getAllFoldersAction } from "../../../../actions/folders/getAllFolders";
 import { moveVideosToFolderAction } from "../../../../actions/folders/moveVideosToFolder";
 import { useDashboardContext } from "../Contexts";
 import { toast } from "sonner";
+import { Effect } from "effect";
+import { useQueryClient } from "@tanstack/react-query";
 
 type FolderWithChildren = {
   id: string;
@@ -35,7 +38,6 @@ interface FolderSelectionDialogProps {
   open: boolean;
   onClose: () => void;
   onConfirm: (folderId: string | null) => void;
-  loading?: boolean;
   selectedCount: number;
   videoIds: string[];
 }
@@ -44,7 +46,6 @@ export function FolderSelectionDialog({
   open,
   onClose,
   onConfirm,
-  loading = false,
   selectedCount,
   videoIds,
 }: FolderSelectionDialogProps) {
@@ -53,31 +54,67 @@ export function FolderSelectionDialog({
     new Set()
   );
   const { activeOrganization, activeSpace } = useDashboardContext();
+  const queryClient = useQueryClient();
 
-  // Fetch folders using the server action
-  const { data: foldersData, isLoading } = useQuery({
+  // Fetch folders using useEffectQuery
+  const { data: foldersData, isLoading } = useEffectQuery({
     queryKey: ["folders", activeOrganization?.organization.id, activeSpace?.id],
-    queryFn: async () => {
-      const root = activeSpace?.id
-        ? { variant: "space" as const, spaceId: activeSpace.id }
-        : {
-            variant: "org" as const,
-            organizationId: activeOrganization!.organization.id,
-          };
+    queryFn: () =>
+      Effect.tryPromise(() => {
+        const root = activeSpace?.id
+          ? { variant: "space" as const, spaceId: activeSpace.id }
+          : {
+              variant: "org" as const,
+              organizationId: activeOrganization!.organization.id,
+            };
 
-      const result = await getAllFoldersAction(root);
-
-      if (!result.success) {
-        throw new Error(result.error || "Failed to fetch folders");
-      }
-
-      return result.folders;
-    },
+        return getAllFoldersAction(root).then((result) => {
+          if (!result.success) {
+            throw new Error(result.error || "Failed to fetch folders");
+          }
+          return result.folders;
+        });
+      }),
     enabled: open && !!activeOrganization?.organization.id,
   });
 
   const folders = foldersData || [];
 
+  // Move videos mutation using useEffectMutation
+  const moveVideosMutation = useEffectMutation({
+    mutationFn: (params: {
+      videoIds: string[];
+      targetFolderId: string | null;
+      spaceId?: string | null;
+    }) =>
+      Effect.tryPromise(() =>
+        moveVideosToFolderAction(params).then((result) => {
+          if (!result.success) {
+            throw new Error(result.error || "Failed to move videos");
+          }
+          return result;
+        })
+      ),
+    onSuccess: (result) => {
+      toast.success(result.message);
+      
+      // Invalidate related queries to refresh data
+      queryClient.invalidateQueries({ 
+        queryKey: ["folders", activeOrganization?.organization.id, activeSpace?.id] 
+      });
+      queryClient.invalidateQueries({ 
+        queryKey: ["videos"] 
+      });
+      
+      onConfirm(selectedFolderId);
+      setSelectedFolderId(null);
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : "Failed to move videos");
+      console.error("Error moving videos:", error);
+    },
+  });
+
   // Toggle folder expansion
   const toggleFolderExpansion = (folderId: string) => {
     setExpandedFolders((prev) => {
@@ -165,25 +202,12 @@ export function FolderSelectionDialog({
     );
   };
 
-  const handleConfirm = async () => {
-    try {
-      const result = await moveVideosToFolderAction({
-        videoIds,
-        targetFolderId: selectedFolderId,
-        spaceId: activeSpace?.id,
-      });
-
-      if (result.success) {
-        toast.success(result.message);
-        onConfirm(selectedFolderId);
-        setSelectedFolderId(null);
-      } else {
-        toast.error(result.error);
-      }
-    } catch (error) {
-      toast.error("Failed to move videos");
-      console.error("Error moving videos:", error);
-    }
+  const handleConfirm = () => {
+    moveVideosMutation.mutate({
+      videoIds,
+      targetFolderId: selectedFolderId,
+      spaceId: activeSpace?.id,
+    });
   };
 
   const handleCancel = () => {
@@ -267,7 +291,7 @@ export function FolderSelectionDialog({
             onClick={handleCancel}
             variant="gray"
             size="sm"
-            disabled={loading}
+            disabled={moveVideosMutation.isPending}
           >
             Cancel
           </Button>
@@ -275,10 +299,10 @@ export function FolderSelectionDialog({
             onClick={handleConfirm}
             variant="dark"
             size="sm"
-            spinner={loading}
-            disabled={loading}
+            spinner={moveVideosMutation.isPending}
+            disabled={moveVideosMutation.isPending}
           >
-            {loading ? "Moving..." : "Move"}
+            {moveVideosMutation.isPending ? "Moving..." : "Move"}
           </Button>
         </DialogFooter>
       </DialogContent>
diff --git a/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx b/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
index 6ccf19909b..17dcfd0d61 100644
--- a/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
+++ b/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
@@ -120,7 +120,6 @@ export const SelectedCapsBar = ({
               onClose={() => setMoveDialogOpen(false)}
               onConfirm={handleMoveToFolder}
               selectedCount={selectedCaps.length}
-              loading={false}
               videoIds={selectedCaps}
             />
           </div>
diff --git a/apps/web/lib/folder.ts b/apps/web/lib/folder.ts
index 2d32ced62d..85a15483b7 100644
--- a/apps/web/lib/folder.ts
+++ b/apps/web/lib/folder.ts
@@ -16,7 +16,7 @@ import {
 import { Database } from "@cap/web-backend";
 import type { Video } from "@cap/web-domain";
 import { CurrentUser, Folder } from "@cap/web-domain";
-import { and, desc, eq, inArray } from "drizzle-orm";
+import { and, desc, eq, inArray, isNull } from "drizzle-orm";
 import { sql } from "drizzle-orm/sql";
 import { Effect } from "effect";
 import { revalidatePath } from "next/cache";
@@ -298,7 +298,7 @@ export const getChildFolders = Effect.fn(function* (
           eq(folders.organizationId, user.activeOrganizationId),
           root.variant === "space"
             ? eq(folders.spaceId, root.spaceId)
-            : undefined
+            : isNull(folders.spaceId)
         )
       )
   );
@@ -336,7 +336,7 @@ export const getAllFolders = Effect.fn(function* (
           eq(folders.organizationId, user.activeOrganizationId),
           root.variant === "space"
             ? eq(folders.spaceId, root.spaceId)
-            : undefined
+            : isNull(folders.spaceId)
         )
       )
   );
@@ -424,6 +424,13 @@ export const moveVideosToFolder = Effect.fn(function* (
     if (root?.variant === "space" && targetFolder.spaceId !== root.spaceId) {
       throw new Error("Target folder does not belong to the specified space");
     }
+
+    // Block moves into space folders when not operating in that space
+    if (root?.variant !== "space" && targetFolder.spaceId !== null) {
+      throw new Error(
+        "Target folder is scoped to a space and cannot be used here"
+      );
+    }
   }
 
   // Determine original folder ids and perform the move based on context

From c2d560649346526c3022bf10880e8a8ed35eed3c Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Sat, 4 Oct 2025 14:32:21 +0100
Subject: [PATCH 6/9] refactor: improve folder security and cache management

- Remove inline comments per coding guidelines
- Add scope isolation with isNull(folders.spaceId) filters
- Implement permission checks with SpacesPolicy validation
- Replace broad cache invalidation with targeted setQueryData updates
- Return detailed result data for efficient cache mutations
- Ensure space context validation to prevent cross-scope moves
---
 .../web/actions/folders/moveVideosToFolder.ts |  8 +--
 .../_components/FolderSelectionDialog.tsx     | 70 +++++++++++++------
 2 files changed, 52 insertions(+), 26 deletions(-)

diff --git a/apps/web/actions/folders/moveVideosToFolder.ts b/apps/web/actions/folders/moveVideosToFolder.ts
index 7570deb276..83a2edaeab 100644
--- a/apps/web/actions/folders/moveVideosToFolder.ts
+++ b/apps/web/actions/folders/moveVideosToFolder.ts
@@ -4,7 +4,7 @@ import { getCurrentUser } from "@cap/database/auth/session";
 import { CurrentUser, Video, Folder, Policy } from "@cap/web-domain";
 import { SpacesPolicy } from "@cap/web-backend";
 import { Effect } from "effect";
-import { moveVideosToFolder, getFolderById } from "../../lib/folder";
+import { moveVideosToFolder } from "../../lib/folder";
 import { runPromise } from "../../lib/server";
 import { revalidatePath } from "next/cache";
 
@@ -37,12 +37,10 @@ export async function moveVideosToFolderAction({
       ? { variant: "space" as const, spaceId }
       : { variant: "org" as const, organizationId: user.activeOrganizationId };
 
-    // Create effect with permission checks
     const moveVideosEffect = spaceId
       ? Effect.gen(function* () {
           const spacesPolicy = yield* SpacesPolicy;
 
-          // Perform the folder move operation
           return yield* moveVideosToFolder(
             typedVideoIds,
             typedTargetFolderId,
@@ -55,7 +53,6 @@ export async function moveVideosToFolderAction({
 
     const result = await runPromise(moveVideosEffect);
 
-    // Revalidate paths
     revalidatePath("/dashboard/caps");
 
     if (spaceId) {
@@ -86,6 +83,9 @@ export async function moveVideosToFolderAction({
       message: `Successfully moved ${result.movedCount} video${
         result.movedCount !== 1 ? "s" : ""
       } to ${result.targetFolderId ? "folder" : "root"}`,
+      movedCount: result.movedCount,
+      originalFolderIds: result.originalFolderIds,
+      targetFolderId: result.targetFolderId,
     };
   } catch (error) {
     console.error("Error moving videos to folder:", error);
diff --git a/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx b/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
index c42154faa0..d04c5b4c11 100644
--- a/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
+++ b/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
@@ -15,7 +15,6 @@ import {
 } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { useEffectMutation, useEffectQuery } from "@/lib/EffectRuntime";
-import { withRpc } from "@/lib/Rpcs";
 import { useState } from "react";
 import { getAllFoldersAction } from "../../../../actions/folders/getAllFolders";
 import { moveVideosToFolderAction } from "../../../../actions/folders/moveVideosToFolder";
@@ -56,7 +55,6 @@ export function FolderSelectionDialog({
   const { activeOrganization, activeSpace } = useDashboardContext();
   const queryClient = useQueryClient();
 
-  // Fetch folders using useEffectQuery
   const { data: foldersData, isLoading } = useEffectQuery({
     queryKey: ["folders", activeOrganization?.organization.id, activeSpace?.id],
     queryFn: () =>
@@ -80,7 +78,6 @@ export function FolderSelectionDialog({
 
   const folders = foldersData || [];
 
-  // Move videos mutation using useEffectMutation
   const moveVideosMutation = useEffectMutation({
     mutationFn: (params: {
       videoIds: string[];
@@ -97,25 +94,62 @@ export function FolderSelectionDialog({
       ),
     onSuccess: (result) => {
       toast.success(result.message);
-      
-      // Invalidate related queries to refresh data
-      queryClient.invalidateQueries({ 
-        queryKey: ["folders", activeOrganization?.organization.id, activeSpace?.id] 
-      });
-      queryClient.invalidateQueries({ 
-        queryKey: ["videos"] 
-      });
-      
+
+      const foldersQueryKey = [
+        "folders",
+        activeOrganization?.organization.id,
+        activeSpace?.id,
+      ];
+
+      queryClient.setQueryData(
+        foldersQueryKey,
+        (oldFolders: FolderWithChildren[] | undefined) => {
+          if (!oldFolders) return oldFolders;
+
+          return oldFolders.map((folder) => ({
+            ...folder,
+            videoCount: result.originalFolderIds.includes(folder.id)
+              ? Math.max(0, folder.videoCount - videoIds.length)
+              : folder.id === selectedFolderId
+              ? folder.videoCount + videoIds.length
+              : folder.videoCount,
+            children:
+              folder.children?.map((child) => ({
+                ...child,
+                videoCount: result.originalFolderIds.includes(child.id)
+                  ? Math.max(0, child.videoCount - videoIds.length)
+                  : child.id === selectedFolderId
+                  ? child.videoCount + videoIds.length
+                  : child.videoCount,
+              })) || [],
+          }));
+        }
+      );
+
+      queryClient.setQueriesData(
+        { queryKey: ["videos"] },
+        (oldVideos: any[] | undefined) => {
+          if (!oldVideos) return oldVideos;
+
+          return oldVideos.map((video) =>
+            videoIds.includes(video.id)
+              ? { ...video, folderId: selectedFolderId }
+              : video
+          );
+        }
+      );
+
       onConfirm(selectedFolderId);
       setSelectedFolderId(null);
     },
     onError: (error) => {
-      toast.error(error instanceof Error ? error.message : "Failed to move videos");
+      toast.error(
+        error instanceof Error ? error.message : "Failed to move videos"
+      );
       console.error("Error moving videos:", error);
     },
   });
 
-  // Toggle folder expansion
   const toggleFolderExpansion = (folderId: string) => {
     setExpandedFolders((prev) => {
       const newSet = new Set(prev);
@@ -128,7 +162,6 @@ export function FolderSelectionDialog({
     });
   };
 
-  // Render folder with improved design
   const renderFolder = (folder: FolderWithChildren, depth = 0) => {
     const hasChildren = folder.children && folder.children.length > 0;
     const isExpanded = expandedFolders.has(folder.id);
@@ -149,7 +182,6 @@ export function FolderSelectionDialog({
           `}
           style={{ marginLeft: `${depth * 16}px` }}
         >
-          {/* Expand/Collapse button */}
           {hasChildren ? (
             <button
               onClick={(e) => {
@@ -167,7 +199,6 @@ export function FolderSelectionDialog({
             <div className="w-5 h-5 flex-shrink-0" />
           )}
 
-          {/* Folder icon */}
           <div className="flex items-center gap-2 flex-1">
             <div className="flex-shrink-0 w-7 h-7 rounded-md bg-gray-2 border border-gray-6 shadow-sm flex items-center justify-center">
               <FontAwesomeIcon
@@ -184,7 +215,6 @@ export function FolderSelectionDialog({
             </div>
           </div>
 
-          {/* Selection indicator */}
           {isSelected && (
             <div className="ml-auto w-5 h-5 rounded-full bg-blue-9 flex items-center justify-center">
               <div className="w-2.5 h-2.5 bg-white rounded-full" />
@@ -192,7 +222,6 @@ export function FolderSelectionDialog({
           )}
         </div>
 
-        {/* Render children if expanded */}
         {hasChildren && isExpanded && (
           <div className="mt-1">
             {folder.children.map((child) => renderFolder(child, depth + 1))}
@@ -229,7 +258,6 @@ export function FolderSelectionDialog({
           </p>
 
           <div className="space-y-2 max-h-[320px] overflow-y-auto custom-scroll">
-            {/* Option to move to root (no folder) */}
             <div
               onClick={() => setSelectedFolderId(null)}
               className={`
@@ -261,14 +289,12 @@ export function FolderSelectionDialog({
               )}
             </div>
 
-            {/* Loading state */}
             {isLoading && (
               <div className="text-center py-4">
                 <p className="text-sm text-gray-10">Loading folders...</p>
               </div>
             )}
 
-            {/* Available folders with hierarchy */}
             {!isLoading &&
               folders.map((folder: FolderWithChildren) => renderFolder(folder))}
           </div>

From 0ab0856d27903fdaf5cc78add29ace12982e5810 Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Sat, 4 Oct 2025 14:50:26 +0100
Subject: [PATCH 7/9] refactor: rename FolderSelectionDialog to follow
 kebab-case convention

- Rename FolderSelectionDialog.tsx to folder-selection-dialog.tsx per coding guidelines
- Update import path in SelectedCapsBar.tsx to use new kebab-case filename
- Preserve git history using git mv command
---
 .../{FolderSelectionDialog.tsx => folder-selection-dialog.tsx}  | 0
 .../web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx | 2 +-
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename apps/web/app/(org)/dashboard/_components/{FolderSelectionDialog.tsx => folder-selection-dialog.tsx} (100%)

diff --git a/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx b/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
similarity index 100%
rename from apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
rename to apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
diff --git a/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx b/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
index 17dcfd0d61..eaf68153bc 100644
--- a/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
+++ b/apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
@@ -10,7 +10,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import NumberFlow from "@number-flow/react";
 import { AnimatePresence, motion } from "framer-motion";
 import { ConfirmationDialog } from "@/app/(org)/dashboard/_components/ConfirmationDialog";
-import { FolderSelectionDialog } from "@/app/(org)/dashboard/_components/FolderSelectionDialog";
+import { FolderSelectionDialog } from "@/app/(org)/dashboard/_components/folder-selection-dialog";
 
 interface SelectedCapsBarProps {
   selectedCaps: string[];

From b84ad4b20d6b6990119f47661c5dfc6614ff8f78 Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Sat, 4 Oct 2025 15:08:01 +0100
Subject: [PATCH 8/9] fix: implement accurate per-folder cache deltas and
 recursive updates

- Replace over-deduction logic with precise per-folder video count deltas
- Add videoCountDeltas to moveVideosToFolder return value with exact decrements/increments
- Implement recursive applyFolderDeltas helper to update entire folder tree
- Fix multi-source folder moves by calculating actual videos moved per folder
- Ensure nested folders at all depths get updated correctly
- Prevent cache staleness in grandchildren and deeper folder descendants
---
 .../web/actions/folders/moveVideosToFolder.ts |  1 +
 .../_components/FolderSelectionDialog.tsx     |  0
 .../_components/folder-selection-dialog.tsx   | 31 +++++++--------
 apps/web/lib/folder.ts                        | 39 ++++++++++++++++---
 4 files changed, 47 insertions(+), 24 deletions(-)
 create mode 100644 apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx

diff --git a/apps/web/actions/folders/moveVideosToFolder.ts b/apps/web/actions/folders/moveVideosToFolder.ts
index 83a2edaeab..7d6ef3aec1 100644
--- a/apps/web/actions/folders/moveVideosToFolder.ts
+++ b/apps/web/actions/folders/moveVideosToFolder.ts
@@ -86,6 +86,7 @@ export async function moveVideosToFolderAction({
       movedCount: result.movedCount,
       originalFolderIds: result.originalFolderIds,
       targetFolderId: result.targetFolderId,
+      videoCountDeltas: result.videoCountDeltas,
     };
   } catch (error) {
     console.error("Error moving videos to folder:", error);
diff --git a/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx b/apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx b/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
index d04c5b4c11..3658d3984b 100644
--- a/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
+++ b/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
@@ -101,28 +101,23 @@ export function FolderSelectionDialog({
         activeSpace?.id,
       ];
 
+      const applyFolderDeltas = (
+        nodes: FolderWithChildren[],
+        deltas: Record<string, number>
+      ): FolderWithChildren[] =>
+        nodes.map((node) => ({
+          ...node,
+          videoCount: Math.max(0, node.videoCount + (deltas[node.id] ?? 0)),
+          children: node.children
+            ? applyFolderDeltas(node.children, deltas)
+            : [],
+        }));
+
       queryClient.setQueryData(
         foldersQueryKey,
         (oldFolders: FolderWithChildren[] | undefined) => {
           if (!oldFolders) return oldFolders;
-
-          return oldFolders.map((folder) => ({
-            ...folder,
-            videoCount: result.originalFolderIds.includes(folder.id)
-              ? Math.max(0, folder.videoCount - videoIds.length)
-              : folder.id === selectedFolderId
-              ? folder.videoCount + videoIds.length
-              : folder.videoCount,
-            children:
-              folder.children?.map((child) => ({
-                ...child,
-                videoCount: result.originalFolderIds.includes(child.id)
-                  ? Math.max(0, child.videoCount - videoIds.length)
-                  : child.id === selectedFolderId
-                  ? child.videoCount + videoIds.length
-                  : child.videoCount,
-              })) || [],
-          }));
+          return applyFolderDeltas(oldFolders, result.videoCountDeltas);
         }
       );
 
diff --git a/apps/web/lib/folder.ts b/apps/web/lib/folder.ts
index 85a15483b7..019e120035 100644
--- a/apps/web/lib/folder.ts
+++ b/apps/web/lib/folder.ts
@@ -435,6 +435,7 @@ export const moveVideosToFolder = Effect.fn(function* (
 
   // Determine original folder ids and perform the move based on context
   let originalFolderIds: (string | null)[] = [];
+  const videoCountDeltas: Record<string, number> = {};
 
   if (root?.variant === "space") {
     // Collect originals from space_videos
@@ -459,9 +460,20 @@ export const moveVideosToFolder = Effect.fn(function* (
         "Some videos are not in the specified space or you don't have permission to move them"
       );
     }
-    originalFolderIds = [
-      ...new Set(spaceRows.map((r) => r.folderId).filter(Boolean)),
-    ];
+    
+    // Calculate per-folder decrements
+    const folderCounts = new Map<string, number>();
+    spaceRows.forEach((row) => {
+      if (row.folderId) {
+        folderCounts.set(row.folderId, (folderCounts.get(row.folderId) || 0) + 1);
+      }
+    });
+    
+    folderCounts.forEach((count, folderId) => {
+      videoCountDeltas[folderId] = -count;
+    });
+    
+    originalFolderIds = [...folderCounts.keys()];
 
     // Update per-space folder placement
     yield* db.execute((db) =>
@@ -477,9 +489,18 @@ export const moveVideosToFolder = Effect.fn(function* (
     );
   } else {
     // ORG/global placement via videos.folderId
-    originalFolderIds = [
-      ...new Set(existingVideos.map((v) => v.folderId).filter(Boolean)),
-    ];
+    const folderCounts = new Map<string, number>();
+    existingVideos.forEach((video) => {
+      if (video.folderId) {
+        folderCounts.set(video.folderId, (folderCounts.get(video.folderId) || 0) + 1);
+      }
+    });
+    
+    folderCounts.forEach((count, folderId) => {
+      videoCountDeltas[folderId] = -count;
+    });
+    
+    originalFolderIds = [...folderCounts.keys()];
 
     yield* db.execute((db) =>
       db
@@ -492,9 +513,15 @@ export const moveVideosToFolder = Effect.fn(function* (
     );
   }
 
+  // Add increment for target folder if specified
+  if (targetFolderId) {
+    videoCountDeltas[targetFolderId] = (videoCountDeltas[targetFolderId] || 0) + videoIds.length;
+  }
+
   return {
     movedCount: videoIds.length,
     originalFolderIds,
     targetFolderId,
+    videoCountDeltas,
   };
 });

From e3606516696735534bcc8f586a748ef8f7e45205 Mon Sep 17 00:00:00 2001
From: Eneji Victor <enejivic@gmail.com>
Date: Sat, 4 Oct 2025 15:31:57 +0100
Subject: [PATCH 9/9] renaming moveVideosToFolder to move-videos-to-folder

---
 ...osToFolder.ts => move-videos-to-folder.ts} |  0
 .../_components/folder-selection-dialog.tsx   |  2 +-
 apps/web/lib/folder.ts                        | 23 ++++++++++++-------
 3 files changed, 16 insertions(+), 9 deletions(-)
 rename apps/web/actions/folders/{moveVideosToFolder.ts => move-videos-to-folder.ts} (100%)

diff --git a/apps/web/actions/folders/moveVideosToFolder.ts b/apps/web/actions/folders/move-videos-to-folder.ts
similarity index 100%
rename from apps/web/actions/folders/moveVideosToFolder.ts
rename to apps/web/actions/folders/move-videos-to-folder.ts
diff --git a/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx b/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
index 3658d3984b..0b4d5fc405 100644
--- a/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
+++ b/apps/web/app/(org)/dashboard/_components/folder-selection-dialog.tsx
@@ -17,7 +17,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { useEffectMutation, useEffectQuery } from "@/lib/EffectRuntime";
 import { useState } from "react";
 import { getAllFoldersAction } from "../../../../actions/folders/getAllFolders";
-import { moveVideosToFolderAction } from "../../../../actions/folders/moveVideosToFolder";
+import { moveVideosToFolderAction } from "../../../../actions/folders/move-videos-to-folder";
 import { useDashboardContext } from "../Contexts";
 import { toast } from "sonner";
 import { Effect } from "effect";
diff --git a/apps/web/lib/folder.ts b/apps/web/lib/folder.ts
index 019e120035..53e4ebbb54 100644
--- a/apps/web/lib/folder.ts
+++ b/apps/web/lib/folder.ts
@@ -460,19 +460,22 @@ export const moveVideosToFolder = Effect.fn(function* (
         "Some videos are not in the specified space or you don't have permission to move them"
       );
     }
-    
+
     // Calculate per-folder decrements
     const folderCounts = new Map<string, number>();
     spaceRows.forEach((row) => {
       if (row.folderId) {
-        folderCounts.set(row.folderId, (folderCounts.get(row.folderId) || 0) + 1);
+        folderCounts.set(
+          row.folderId,
+          (folderCounts.get(row.folderId) || 0) + 1
+        );
       }
     });
-    
+
     folderCounts.forEach((count, folderId) => {
       videoCountDeltas[folderId] = -count;
     });
-    
+
     originalFolderIds = [...folderCounts.keys()];
 
     // Update per-space folder placement
@@ -492,14 +495,17 @@ export const moveVideosToFolder = Effect.fn(function* (
     const folderCounts = new Map<string, number>();
     existingVideos.forEach((video) => {
       if (video.folderId) {
-        folderCounts.set(video.folderId, (folderCounts.get(video.folderId) || 0) + 1);
+        folderCounts.set(
+          video.folderId,
+          (folderCounts.get(video.folderId) || 0) + 1
+        );
       }
     });
-    
+
     folderCounts.forEach((count, folderId) => {
       videoCountDeltas[folderId] = -count;
     });
-    
+
     originalFolderIds = [...folderCounts.keys()];
 
     yield* db.execute((db) =>
@@ -515,7 +521,8 @@ export const moveVideosToFolder = Effect.fn(function* (
 
   // Add increment for target folder if specified
   if (targetFolderId) {
-    videoCountDeltas[targetFolderId] = (videoCountDeltas[targetFolderId] || 0) + videoIds.length;
+    videoCountDeltas[targetFolderId] =
+      (videoCountDeltas[targetFolderId] || 0) + videoIds.length;
   }
 
   return {
