From 91cadee5f534a95e49fd5b8be7bffa0fd1e15d10 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:03:53 +0300 Subject: [PATCH 01/12] add videos button + show folder the video is in --- apps/web/actions/folders/add-videos.ts | 125 ++++++++++++++++++ apps/web/actions/folders/get-folder-videos.ts | 40 ++++++ apps/web/actions/folders/remove-videos.ts | 91 +++++++++++++ apps/web/actions/videos/get-user-videos.ts | 20 ++- .../components/AddVideosDialogBase.tsx | 6 +- .../spaces/[spaceId]/components/VideoCard.tsx | 66 +++++++-- .../folder/[folderId]/AddVideosButton.tsx | 46 +++++++ .../[spaceId]/folder/[folderId]/page.tsx | 5 + 8 files changed, 381 insertions(+), 18 deletions(-) create mode 100644 apps/web/actions/folders/add-videos.ts create mode 100644 apps/web/actions/folders/get-folder-videos.ts create mode 100644 apps/web/actions/folders/remove-videos.ts create mode 100644 apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx diff --git a/apps/web/actions/folders/add-videos.ts b/apps/web/actions/folders/add-videos.ts new file mode 100644 index 0000000000..7d5418e028 --- /dev/null +++ b/apps/web/actions/folders/add-videos.ts @@ -0,0 +1,125 @@ +"use server"; + +import { db } from "@cap/database"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { nanoId } from "@cap/database/helpers"; +import { folders, spaceVideos, videos } from "@cap/database/schema"; +import type { Folder, Space, Video } from "@cap/web-domain"; +import { and, eq, inArray } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; + +export async function addVideosToFolder( + folderId: Folder.FolderId, + videoIds: Video.VideoId[], +) { + try { + const user = await getCurrentUser(); + + if (!user || !user.id) { + throw new Error("Unauthorized"); + } + + if (!folderId || !videoIds || videoIds.length === 0) { + throw new Error("Missing required data"); + } + + // Verify folder exists and is accessible + const [folder] = await db() + .select({ id: folders.id, spaceId: folders.spaceId }) + .from(folders) + .where(eq(folders.id, folderId)); + + if (!folder) { + throw new Error("Folder not found"); + } + + // Only allow updating videos the user owns + const userVideos = await db() + .select({ id: videos.id }) + .from(videos) + .where(and(eq(videos.ownerId, user.id), inArray(videos.id, videoIds))); + + const validVideoIds = userVideos.map((v) => v.id); + + if (validVideoIds.length === 0) { + throw new Error("No valid videos found"); + } + + // Update the video's folderId + await db() + .update(videos) + .set({ folderId: folderId, updatedAt: new Date() }) + .where(inArray(videos.id, validVideoIds)); + + // If this folder belongs to a space, ensure spaceVideos entry exists and set folderId in that relation + if (folder.spaceId) { + // Find existing relations + const existingRelations = await db() + .select({ videoId: spaceVideos.videoId }) + .from(spaceVideos) + .where( + and( + eq( + spaceVideos.spaceId, + folder.spaceId as Space.SpaceIdOrOrganisationId, + ), + inArray(spaceVideos.videoId, validVideoIds), + ), + ); + + const existingIds = new Set(existingRelations.map((r) => r.videoId)); + const toInsert = validVideoIds.filter((id) => !existingIds.has(id)); + + if (toInsert.length > 0) { + const spaceIdValue = folder.spaceId as Space.SpaceIdOrOrganisationId; + await db() + .insert(spaceVideos) + .values( + toInsert.map((id) => ({ + id: nanoId(), + videoId: id, + spaceId: spaceIdValue, + addedById: user.id, + folderId, + })), + ); + } + + // Update folderId for all valid videos in this space + await db() + .update(spaceVideos) + .set({ folderId }) + .where( + and( + eq( + spaceVideos.spaceId, + folder.spaceId as Space.SpaceIdOrOrganisationId, + ), + inArray(spaceVideos.videoId, validVideoIds), + ), + ); + } + + // Revalidate relevant paths + revalidatePath(`/dashboard/caps`); + revalidatePath(`/dashboard/folder/${folderId}`); + if (folder.spaceId) { + revalidatePath(`/dashboard/spaces/${folder.spaceId}/folder/${folderId}`); + } + + return { + success: true, + message: `${validVideoIds.length} video${validVideoIds.length === 1 ? "" : "s"} added to folder`, + addedCount: validVideoIds.length, + }; + } catch (error) { + console.error("Error adding videos to folder:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to add videos to folder", + }; + } +} diff --git a/apps/web/actions/folders/get-folder-videos.ts b/apps/web/actions/folders/get-folder-videos.ts new file mode 100644 index 0000000000..7002d44d0e --- /dev/null +++ b/apps/web/actions/folders/get-folder-videos.ts @@ -0,0 +1,40 @@ +"use server"; + +import { db } from "@cap/database"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { videos } from "@cap/database/schema"; +import type { Folder, Video } from "@cap/web-domain"; +import { eq } from "drizzle-orm"; + +export async function getFolderVideoIds(folderId: Folder.FolderId) { + try { + const user = await getCurrentUser(); + + if (!user || !user.id) { + throw new Error("Unauthorized"); + } + + if (!folderId) { + throw new Error("Folder ID is required"); + } + + const rows = await db() + .select({ id: videos.id }) + .from(videos) + .where(eq(videos.folderId, folderId)); + + return { + success: true, + data: rows.map((r) => r.id as Video.VideoId), + }; + } catch (error) { + console.error("Error fetching folder video IDs:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to fetch folder videos", + }; + } +} diff --git a/apps/web/actions/folders/remove-videos.ts b/apps/web/actions/folders/remove-videos.ts new file mode 100644 index 0000000000..eb372bb7cb --- /dev/null +++ b/apps/web/actions/folders/remove-videos.ts @@ -0,0 +1,91 @@ +"use server"; + +import { db } from "@cap/database"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { folders, spaceVideos, videos } from "@cap/database/schema"; +import type { Folder, Video } from "@cap/web-domain"; +import { and, eq, inArray } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; + +export async function removeVideosFromFolder( + folderId: Folder.FolderId, + videoIds: Video.VideoId[], +) { + try { + const user = await getCurrentUser(); + + if (!user || !user.id) { + throw new Error("Unauthorized"); + } + + if (!folderId || !videoIds || videoIds.length === 0) { + throw new Error("Missing required data"); + } + + // Verify folder exists and is accessible + const [folder] = await db() + .select({ id: folders.id, spaceId: folders.spaceId }) + .from(folders) + .where(eq(folders.id, folderId)); + + if (!folder) { + throw new Error("Folder not found"); + } + + // Only allow updating videos the user owns + const userVideos = await db() + .select({ id: videos.id }) + .from(videos) + .where(and(eq(videos.ownerId, user.id), inArray(videos.id, videoIds))); + + const validVideoIds = userVideos.map((v) => v.id); + + if (validVideoIds.length === 0) { + throw new Error("No valid videos found"); + } + + // Clear the folderId on the videos + await db() + .update(videos) + .set({ folderId: null, updatedAt: new Date() }) + .where( + and(inArray(videos.id, validVideoIds), eq(videos.folderId, folderId)), + ); + + // If folder belongs to a space, also clear the folderId in spaceVideos relation + if (folder.spaceId) { + await db() + .update(spaceVideos) + .set({ folderId: null }) + .where( + and( + eq(spaceVideos.spaceId, folder.spaceId), + inArray(spaceVideos.videoId, validVideoIds), + eq(spaceVideos.folderId, folderId), + ), + ); + } + + // Revalidate relevant paths + revalidatePath(`/dashboard/caps`); + revalidatePath(`/dashboard/folder/${folderId}`); + if (folder.spaceId) { + revalidatePath(`/dashboard/spaces/${folder.spaceId}/folder/${folderId}`); + } + + return { + success: true, + message: `${validVideoIds.length} video${validVideoIds.length === 1 ? "" : "s"} removed from folder`, + removedCount: validVideoIds.length, + }; + } catch (error) { + console.error("Error removing videos from folder:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to remove videos from folder", + }; + } +} diff --git a/apps/web/actions/videos/get-user-videos.ts b/apps/web/actions/videos/get-user-videos.ts index d12d4c0431..cdf7808ae7 100644 --- a/apps/web/actions/videos/get-user-videos.ts +++ b/apps/web/actions/videos/get-user-videos.ts @@ -2,7 +2,13 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { comments, users, videos, videoUploads } from "@cap/database/schema"; +import { + comments, + folders, + users, + videos, + videoUploads, +} from "@cap/database/schema"; import { desc, eq, sql } from "drizzle-orm"; export async function getUserVideos(limit?: number) { @@ -25,6 +31,8 @@ export async function getUserVideos(limit?: number) { totalComments: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'text' THEN ${comments.id} END)`, totalReactions: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'emoji' THEN ${comments.id} END)`, ownerName: users.name, + folderName: folders.name, + folderColor: folders.color, effectiveDate: sql` COALESCE( JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), @@ -39,6 +47,7 @@ export async function getUserVideos(limit?: number) { .leftJoin(comments, eq(videos.id, comments.videoId)) .leftJoin(users, eq(videos.ownerId, users.id)) .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) + .leftJoin(folders, eq(videos.folderId, folders.id)) .where(eq(videos.ownerId, userId)) .groupBy( videos.id, @@ -47,6 +56,8 @@ export async function getUserVideos(limit?: number) { videos.createdAt, videos.metadata, users.name, + folders.name, + folders.color, ) .orderBy( desc(sql`COALESCE( @@ -57,12 +68,15 @@ export async function getUserVideos(limit?: number) { .limit(limit || 20); const processedVideoData = videoData.map((video) => { - const { effectiveDate, ...videoWithoutEffectiveDate } = video; + const { effectiveDate: _effectiveDate, ...videoWithoutEffectiveDate } = + video; return { ...videoWithoutEffectiveDate, ownerName: video.ownerName ?? "", + folderName: video.folderName ?? null, + folderColor: video.folderColor ?? null, metadata: video.metadata as - | { customCreatedAt?: string; [key: string]: any } + | { customCreatedAt?: string; [key: string]: unknown } | undefined, }; }); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx index 3ec721dab6..5fdaeeeb65 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx @@ -43,6 +43,8 @@ export interface VideoData { totalComments: number; totalReactions: number; ownerName: string; + folderName?: string | null; + folderColor?: "normal" | "blue" | "red" | "yellow" | null; metadata?: { customCreatedAt?: string; }; @@ -101,6 +103,7 @@ function AddVideosDialogBase({ }, enabled: open, refetchOnWindowFocus: false, + refetchOnMount: true, gcTime: 1000 * 60 * 5, }); @@ -189,6 +192,7 @@ function AddVideosDialogBase({ updateVideosMutation.mutate({ toAdd, toRemove }); }; + // Reset state when dialog closes useEffect(() => { if (!open) { setSelectedVideos([]); @@ -203,7 +207,7 @@ function AddVideosDialogBase({ } description={ - "Find and add videos you have previously recorded to share with people in this " + + "Find and add videos you have previously recorded to share with people in " + entityName + "." } diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx index 590f1f75db..068861fa55 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx @@ -1,9 +1,11 @@ +import { Fit, Layout, useRive } from "@rive-app/react-canvas"; import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; import { Check, Minus, Plus } from "lucide-react"; import moment from "moment"; import type React from "react"; import { memo, useState } from "react"; +import { useTheme } from "@/app/(org)/dashboard/Contexts"; import { Tooltip } from "@/components/Tooltip"; import { type ImageLoadingStatus, @@ -21,6 +23,7 @@ interface VideoCardProps { const VideoCard: React.FC = memo( ({ video, isSelected, onToggle, isAlreadyInEntity, className }) => { + const { theme } = useTheme(); const effectiveDate = video.metadata?.customCreatedAt ? new Date(video.metadata.customCreatedAt) : video.createdAt; @@ -28,9 +31,35 @@ const VideoCard: React.FC = memo( const [imageStatus, setImageStatus] = useState("loading"); + const folderColor = video.folderColor || "normal"; + const artboard = + theme === "dark" && folderColor === "normal" + ? "folder" + : folderColor === "normal" + ? "folder-dark" + : `folder-${folderColor}`; + + const { RiveComponent: FolderRive } = useRive({ + src: "/rive/dashboard.riv", + artboard, + animations: "idle", + autoplay: true, + layout: new Layout({ + fit: Fit.Contain, + }), + }); + return (
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onToggle(); + } + }} className={clsx( "flex relative flex-col p-3 w-full min-h-fit rounded-xl border transition-all duration-200 group", className, @@ -115,20 +144,29 @@ const VideoCard: React.FC = memo( />
-
- -

- {video.name} -

-
-

- {moment(effectiveDate).format("MMM D, YYYY")} -

+
+
+ +

+ {video.name} +

+
+

+ {moment(effectiveDate).format("MMM D, YYYY")} +

+
+ {video.folderName && ( +
+ +

+ {video.folderName} +

+
+ )}
); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx new file mode 100644 index 0000000000..7a170a69fd --- /dev/null +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Button } from "@cap/ui"; +import type { Folder } from "@cap/web-domain"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { addVideosToFolder } from "@/actions/folders/add-videos"; +import { getFolderVideoIds } from "@/actions/folders/get-folder-videos"; +import { removeVideosFromFolder } from "@/actions/folders/remove-videos"; +import { getUserVideos } from "@/actions/videos/get-user-videos"; +import AddVideosDialogBase from "../../components/AddVideosDialogBase"; + +export default function AddVideosButton({ + folderId, + folderName, +}: { + folderId: Folder.FolderId; + folderName: string; +}) { + const [open, setOpen] = useState(false); + const router = useRouter(); + + return ( + <> + + setOpen(false)} + entityId={folderId} + entityName={folderName} + onVideosAdded={() => { + router.refresh(); + }} + addVideos={addVideosToFolder} + removeVideos={removeVideosFromFolder} + getVideos={getUserVideos} + getEntityVideoIds={getFolderVideoIds} + /> + + ); +} diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx index 4af71d3a4e..7cde09dd50 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx @@ -17,6 +17,7 @@ import { NewSubfolderButton, } from "../../../../folder/[id]/components"; import FolderVideosSection from "../../../../folder/[id]/components/FolderVideosSection"; +import AddVideosButton from "./AddVideosButton"; const FolderPage = async (props: { params: Promise<{ spaceId: string; folderId: Folder.FolderId }>; @@ -47,6 +48,10 @@ const FolderPage = async (props: {
+
From a3b82f417308e63de83da1ddd74c388ae9d71e2b Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:31:04 +0300 Subject: [PATCH 02/12] some fixes --- apps/web/actions/{videos => spaces}/get-user-videos.ts | 5 ++--- .../spaces/[spaceId]/components/AddVideosDialog.tsx | 2 +- .../spaces/[spaceId]/components/AddVideosDialogBase.tsx | 2 +- .../[spaceId]/components/AddVideosToOrganizationDialog.tsx | 2 +- .../spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) rename apps/web/actions/{videos => spaces}/get-user-videos.ts (96%) diff --git a/apps/web/actions/videos/get-user-videos.ts b/apps/web/actions/spaces/get-user-videos.ts similarity index 96% rename from apps/web/actions/videos/get-user-videos.ts rename to apps/web/actions/spaces/get-user-videos.ts index cdf7808ae7..ec455e753b 100644 --- a/apps/web/actions/videos/get-user-videos.ts +++ b/apps/web/actions/spaces/get-user-videos.ts @@ -11,7 +11,7 @@ import { } from "@cap/database/schema"; import { desc, eq, sql } from "drizzle-orm"; -export async function getUserVideos(limit?: number) { +export async function getUserVideos() { try { const user = await getCurrentUser(); @@ -64,8 +64,7 @@ export async function getUserVideos(limit?: number) { JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), ${videos.createdAt} )`), - ) - .limit(limit || 20); + ); const processedVideoData = videoData.map((video) => { const { effectiveDate: _effectiveDate, ...videoWithoutEffectiveDate } = diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx index 305acfc0b8..e304f562ae 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx @@ -4,8 +4,8 @@ import type { Space } from "@cap/web-domain"; import type React from "react"; import { addVideosToSpace } from "@/actions/spaces/add-videos"; import { getSpaceVideoIds } from "@/actions/spaces/get-space-videos"; +import { getUserVideos } from "@/actions/spaces/get-user-videos"; import { removeVideosFromSpace } from "@/actions/spaces/remove-videos"; -import { getUserVideos } from "@/actions/videos/get-user-videos"; import AddVideosDialogBase from "./AddVideosDialogBase"; interface AddVideosDialogProps { diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx index 5fdaeeeb65..85be6b02ba 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx @@ -31,7 +31,7 @@ interface AddVideosDialogBaseProp { onVideosAdded?: () => void; addVideos: (entityId: T, videoIds: Video.VideoId[]) => Promise; removeVideos: (entityId: T, videoIds: Video.VideoId[]) => Promise; - getVideos: (limit?: number) => Promise; + getVideos: () => Promise; getEntityVideoIds: (entityId: T) => Promise; } diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx index bd5ece90c0..be02ec5df6 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx @@ -5,7 +5,7 @@ import type React from "react"; import { addVideosToOrganization } from "@/actions/organizations/add-videos"; import { getOrganizationVideoIds } from "@/actions/organizations/get-organization-videos"; import { removeVideosFromOrganization } from "@/actions/organizations/remove-videos"; -import { getUserVideos } from "@/actions/videos/get-user-videos"; +import { getUserVideos } from "@/actions/spaces/get-user-videos"; import AddVideosDialogBase from "./AddVideosDialogBase"; interface AddVideosToOrganizationDialogProps { diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx index 7a170a69fd..27cbae3cfc 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx @@ -9,7 +9,7 @@ import { useState } from "react"; import { addVideosToFolder } from "@/actions/folders/add-videos"; import { getFolderVideoIds } from "@/actions/folders/get-folder-videos"; import { removeVideosFromFolder } from "@/actions/folders/remove-videos"; -import { getUserVideos } from "@/actions/videos/get-user-videos"; +import { getUserVideos } from "@/actions/spaces/get-user-videos"; import AddVideosDialogBase from "../../components/AddVideosDialogBase"; export default function AddVideosButton({ @@ -38,7 +38,7 @@ export default function AddVideosButton({ }} addVideos={addVideosToFolder} removeVideos={removeVideosFromFolder} - getVideos={getUserVideos} + getVideos={() => getUserVideos()} getEntityVideoIds={getFolderVideoIds} /> From fc3b392e9500132e10cc2a3d3b0bf03fafea7e51 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Wed, 8 Oct 2025 23:34:03 +0300 Subject: [PATCH 03/12] fixes --- apps/web/actions/folders/add-videos.ts | 50 ++++--------------- apps/web/actions/spaces/add-videos.ts | 23 +-------- apps/web/actions/spaces/get-space-videos.ts | 6 ++- apps/web/actions/spaces/get-user-videos.ts | 10 ++-- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 4 ++ .../[spaceId]/components/AddVideosDialog.tsx | 2 +- .../components/AddVideosDialogBase.tsx | 14 +++--- .../AddVideosToOrganizationDialog.tsx | 12 ++++- .../folder/[folderId]/AddVideosButton.tsx | 8 ++- .../[spaceId]/folder/[folderId]/page.tsx | 1 + .../(org)/dashboard/spaces/[spaceId]/page.tsx | 2 + 11 files changed, 56 insertions(+), 76 deletions(-) diff --git a/apps/web/actions/folders/add-videos.ts b/apps/web/actions/folders/add-videos.ts index 7d5418e028..55c0a9c39a 100644 --- a/apps/web/actions/folders/add-videos.ts +++ b/apps/web/actions/folders/add-videos.ts @@ -2,7 +2,6 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { nanoId } from "@cap/database/helpers"; import { folders, spaceVideos, videos } from "@cap/database/schema"; import type { Folder, Space, Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; @@ -11,6 +10,7 @@ import { revalidatePath } from "next/cache"; export async function addVideosToFolder( folderId: Folder.FolderId, videoIds: Video.VideoId[], + spaceId?: string, ) { try { const user = await getCurrentUser(); @@ -51,41 +51,10 @@ export async function addVideosToFolder( .set({ folderId: folderId, updatedAt: new Date() }) .where(inArray(videos.id, validVideoIds)); - // If this folder belongs to a space, ensure spaceVideos entry exists and set folderId in that relation - if (folder.spaceId) { - // Find existing relations - const existingRelations = await db() - .select({ videoId: spaceVideos.videoId }) - .from(spaceVideos) - .where( - and( - eq( - spaceVideos.spaceId, - folder.spaceId as Space.SpaceIdOrOrganisationId, - ), - inArray(spaceVideos.videoId, validVideoIds), - ), - ); - - const existingIds = new Set(existingRelations.map((r) => r.videoId)); - const toInsert = validVideoIds.filter((id) => !existingIds.has(id)); - - if (toInsert.length > 0) { - const spaceIdValue = folder.spaceId as Space.SpaceIdOrOrganisationId; - await db() - .insert(spaceVideos) - .values( - toInsert.map((id) => ({ - id: nanoId(), - videoId: id, - spaceId: spaceIdValue, - addedById: user.id, - folderId, - })), - ); - } - - // Update folderId for all valid videos in this space + // If this folder belongs to a space and we have a spaceId context, update spaceVideos relation + const effectiveSpaceId = spaceId || folder.spaceId; + if (effectiveSpaceId) { + // Update folderId for videos that are already in this space await db() .update(spaceVideos) .set({ folderId }) @@ -93,7 +62,7 @@ export async function addVideosToFolder( and( eq( spaceVideos.spaceId, - folder.spaceId as Space.SpaceIdOrOrganisationId, + effectiveSpaceId as Space.SpaceIdOrOrganisationId, ), inArray(spaceVideos.videoId, validVideoIds), ), @@ -103,8 +72,11 @@ export async function addVideosToFolder( // Revalidate relevant paths revalidatePath(`/dashboard/caps`); revalidatePath(`/dashboard/folder/${folderId}`); - if (folder.spaceId) { - revalidatePath(`/dashboard/spaces/${folder.spaceId}/folder/${folderId}`); + const effectiveSpaceIdForRevalidate = spaceId || folder.spaceId; + if (effectiveSpaceIdForRevalidate) { + revalidatePath( + `/dashboard/spaces/${effectiveSpaceIdForRevalidate}/folder/${folderId}`, + ); } return { diff --git a/apps/web/actions/spaces/add-videos.ts b/apps/web/actions/spaces/add-videos.ts index bbb5656816..96534374fa 100644 --- a/apps/web/actions/spaces/add-videos.ts +++ b/apps/web/actions/spaces/add-videos.ts @@ -43,26 +43,7 @@ export async function addVideosToSpace( throw new Error("No valid videos found"); } - const existingSpaceVideos = await db() - .select({ videoId: spaceVideos.videoId }) - .from(spaceVideos) - .where( - and( - eq(spaceVideos.spaceId, spaceId), - inArray(spaceVideos.videoId, validVideoIds), - ), - ); - - const existingVideoIds = existingSpaceVideos.map((sv) => sv.videoId); - const newVideoIds = validVideoIds.filter( - (id) => !existingVideoIds.includes(id), - ); - - if (newVideoIds.length === 0) { - return { success: true, message: "Videos already added to space" }; - } - - const spaceVideoEntries = newVideoIds.map((videoId) => ({ + const spaceVideoEntries = validVideoIds.map((videoId) => ({ id: nanoId(), videoId, spaceId, @@ -76,7 +57,7 @@ export async function addVideosToSpace( return { success: true, - message: `${newVideoIds.length} video${newVideoIds.length === 1 ? "" : "s"} added to space`, + message: `${validVideoIds.length} video${validVideoIds.length === 1 ? "" : "s"} added to space`, }; } catch (error) { console.error("Error adding videos to space:", error); diff --git a/apps/web/actions/spaces/get-space-videos.ts b/apps/web/actions/spaces/get-space-videos.ts index 7ebf0d61fe..78653404da 100644 --- a/apps/web/actions/spaces/get-space-videos.ts +++ b/apps/web/actions/spaces/get-space-videos.ts @@ -4,7 +4,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaceVideos } from "@cap/database/schema"; import type { Space } from "@cap/web-domain"; -import { eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; export async function getSpaceVideoIds(spaceId: Space.SpaceIdOrOrganisationId) { try { @@ -23,7 +23,9 @@ export async function getSpaceVideoIds(spaceId: Space.SpaceIdOrOrganisationId) { videoId: spaceVideos.videoId, }) .from(spaceVideos) - .where(eq(spaceVideos.spaceId, spaceId)); + .where( + and(eq(spaceVideos.spaceId, spaceId), isNull(spaceVideos.folderId)), + ); return { success: true, diff --git a/apps/web/actions/spaces/get-user-videos.ts b/apps/web/actions/spaces/get-user-videos.ts index ec455e753b..000b5aa5a8 100644 --- a/apps/web/actions/spaces/get-user-videos.ts +++ b/apps/web/actions/spaces/get-user-videos.ts @@ -5,13 +5,14 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { comments, folders, + spaces, users, videos, videoUploads, } from "@cap/database/schema"; import { desc, eq, sql } from "drizzle-orm"; -export async function getUserVideos() { +export async function getUserVideos(spaceId: string) { try { const user = await getCurrentUser(); @@ -31,8 +32,8 @@ export async function getUserVideos() { totalComments: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'text' THEN ${comments.id} END)`, totalReactions: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'emoji' THEN ${comments.id} END)`, ownerName: users.name, - folderName: folders.name, - folderColor: folders.color, + folderName: sql`CASE WHEN ${folders.spaceId} = ${spaceId} THEN ${folders.name} ELSE NULL END`, + folderColor: sql`CASE WHEN ${folders.spaceId} = ${spaceId} THEN ${folders.color} ELSE NULL END`, effectiveDate: sql` COALESCE( JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), @@ -48,6 +49,7 @@ export async function getUserVideos() { .leftJoin(users, eq(videos.ownerId, users.id)) .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) .leftJoin(folders, eq(videos.folderId, folders.id)) + .leftJoin(spaces, eq(folders.spaceId, spaces.id)) .where(eq(videos.ownerId, userId)) .groupBy( videos.id, @@ -58,6 +60,8 @@ export async function getUserVideos() { users.name, folders.name, folders.color, + folders.spaceId, + videos.folderId, ) .orderBy( desc(sql`COALESCE( diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index cf0b63387f..e53bf4079b 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -46,6 +46,7 @@ export const SharedCaps = ({ data, count, spaceData, + spaceId, spaceMembers, organizationMembers, currentUserId, @@ -57,6 +58,7 @@ export const SharedCaps = ({ count: number; dubApiKeyEnabled: boolean; spaceData?: SpaceData; + spaceId: string; hideSharedWith?: boolean; spaceMembers?: SpaceMemberData[]; organizationMembers?: OrganizationMemberData[]; @@ -192,6 +194,7 @@ export const SharedCaps = ({ organizationId={organizationData.id} organizationName={organizationData.name} onVideosAdded={handleVideosAdded} + spaceId={spaceId} /> )}
@@ -258,6 +261,7 @@ export const SharedCaps = ({ organizationId={organizationData.id} organizationName={organizationData.name} onVideosAdded={handleVideosAdded} + spaceId={spaceId} /> )} ))}
@@ -298,7 +300,7 @@ function AddVideosDialogBase({ entityVideoIds={entityVideoIds || []} height={300} columnCount={3} - rowHeight={200} + rowHeight={230} /> )}
diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx index be02ec5df6..4858daff92 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx @@ -14,11 +14,19 @@ interface AddVideosToOrganizationDialogProps { organizationId: Organisation.OrganisationId; organizationName: string; onVideosAdded?: () => void; + spaceId: string; } export const AddVideosToOrganizationDialog: React.FC< AddVideosToOrganizationDialogProps -> = ({ open, onClose, organizationId, organizationName, onVideosAdded }) => { +> = ({ + open, + onClose, + organizationId, + organizationName, + onVideosAdded, + spaceId, +}) => { return ( getUserVideos(spaceId)} getEntityVideoIds={getOrganizationVideoIds} /> ); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx index 27cbae3cfc..c684a177ba 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx @@ -15,9 +15,11 @@ import AddVideosDialogBase from "../../components/AddVideosDialogBase"; export default function AddVideosButton({ folderId, folderName, + spaceId, }: { folderId: Folder.FolderId; folderName: string; + spaceId: string; }) { const [open, setOpen] = useState(false); const router = useRouter(); @@ -36,9 +38,11 @@ export default function AddVideosButton({ onVideosAdded={() => { router.refresh(); }} - addVideos={addVideosToFolder} + addVideos={(folderIdArg, videoIds) => + addVideosToFolder(folderIdArg, videoIds, spaceId) + } removeVideos={removeVideosFromFolder} - getVideos={() => getUserVideos()} + getVideos={() => getUserVideos(spaceId)} getEntityVideoIds={getFolderVideoIds} /> diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx index 7cde09dd50..ac332ea251 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx @@ -50,6 +50,7 @@ const FolderPage = async (props: { diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx index 7f5d110074..846c09f65c 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx @@ -195,6 +195,7 @@ export default async function SharedCapsPage(props: { data={processedVideoData} count={totalCount} spaceData={space} + spaceId={params.spaceId} dubApiKeyEnabled={!!serverEnv().DUB_API_KEY} spaceMembers={spaceMembersData} organizationMembers={organizationMembersData} @@ -302,6 +303,7 @@ export default async function SharedCapsPage(props: { count={totalCount} hideSharedWith organizationData={organization} + spaceId={params.spaceId} dubApiKeyEnabled={!!serverEnv().DUB_API_KEY} organizationMembers={organizationMembersData} currentUserId={user.id} From 37155d5c5218d036e63a03d26f9a5fb4c51b73c4 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:25:02 +0300 Subject: [PATCH 04/12] add folderId column to shared videos and fix folder --- apps/web/actions/folders/moveVideoToFolder.ts | 34 +++-- apps/web/actions/spaces/add-videos.ts | 11 +- apps/web/actions/spaces/get-user-videos.ts | 130 +++++++++++----- apps/web/app/(org)/dashboard/caps/page.tsx | 14 -- .../[id]/components/FolderVideosSection.tsx | 1 - .../app/(org)/dashboard/folder/[id]/page.tsx | 4 +- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 6 +- .../spaces/[spaceId]/components/VideoCard.tsx | 6 +- .../[spaceId]/folder/[folderId]/page.tsx | 7 +- .../(org)/dashboard/spaces/[spaceId]/page.tsx | 18 ++- apps/web/lib/folder.ts | 16 +- .../database/migrations/meta/_journal.json | 143 +++++++++--------- packages/database/schema.ts | 6 + 13 files changed, 234 insertions(+), 162 deletions(-) diff --git a/apps/web/actions/folders/moveVideoToFolder.ts b/apps/web/actions/folders/moveVideoToFolder.ts index 86a88fbbdd..69c4266db1 100644 --- a/apps/web/actions/folders/moveVideoToFolder.ts +++ b/apps/web/actions/folders/moveVideoToFolder.ts @@ -2,7 +2,12 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { folders, spaceVideos, videos } from "@cap/database/schema"; +import { + folders, + sharedVideos, + spaceVideos, + videos, +} from "@cap/database/schema"; import type { Folder, Video } from "@cap/web-domain"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; @@ -30,6 +35,8 @@ export async function moveVideoToFolder({ const originalFolderId = currentVideo?.folderId; + const isAllSpacesEntry = spaceId === user.activeOrganizationId; + // If folderId is provided, verify it exists and belongs to the same organization if (folderId) { const [folder] = await db() @@ -47,24 +54,29 @@ export async function moveVideoToFolder({ } } - if (spaceId) { + if (spaceId && !isAllSpacesEntry) { await db() .update(spaceVideos) .set({ folderId: folderId === null ? null : folderId, }) .where(eq(spaceVideos.videoId, videoId)); + } else if (spaceId && isAllSpacesEntry) { + await db() + .update(sharedVideos) + .set({ + folderId: folderId === null ? null : folderId, + }) + .where(eq(sharedVideos.videoId, videoId)); + } else { + await db() + .update(videos) + .set({ + folderId: folderId === null ? null : folderId, + }) + .where(eq(videos.id, videoId)); } - // Update the video's folderId - await db() - .update(videos) - .set({ - folderId, - updatedAt: new Date(), - }) - .where(eq(videos.id, videoId)); - // Always revalidate the main caps page revalidatePath(`/dashboard/caps`); diff --git a/apps/web/actions/spaces/add-videos.ts b/apps/web/actions/spaces/add-videos.ts index 96534374fa..ad19d18d46 100644 --- a/apps/web/actions/spaces/add-videos.ts +++ b/apps/web/actions/spaces/add-videos.ts @@ -3,7 +3,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoId } from "@cap/database/helpers"; -import { spaces, spaceVideos, videos } from "@cap/database/schema"; +import { spaceVideos, videos } from "@cap/database/schema"; import type { Space, Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; @@ -23,15 +23,6 @@ export async function addVideosToSpace( throw new Error("Missing required data"); } - const [space] = await db() - .select() - .from(spaces) - .where(eq(spaces.id, spaceId)); - - if (!space) { - throw new Error("Space not found"); - } - const userVideos = await db() .select({ id: videos.id }) .from(videos) diff --git a/apps/web/actions/spaces/get-user-videos.ts b/apps/web/actions/spaces/get-user-videos.ts index 000b5aa5a8..201eb8647a 100644 --- a/apps/web/actions/spaces/get-user-videos.ts +++ b/apps/web/actions/spaces/get-user-videos.ts @@ -5,14 +5,17 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { comments, folders, + sharedVideos, spaces, + spaceVideos, users, videos, videoUploads, } from "@cap/database/schema"; -import { desc, eq, sql } from "drizzle-orm"; +import type { Space } from "@cap/web-domain"; +import { and, desc, eq, sql } from "drizzle-orm"; -export async function getUserVideos(spaceId: string) { +export async function getUserVideos(spaceId: Space.SpaceIdOrOrganisationId) { try { const user = await getCurrentUser(); @@ -21,54 +24,99 @@ export async function getUserVideos(spaceId: string) { } const userId = user.id; + const isAllSpacesEntry = user.activeOrganizationId === spaceId; - const videoData = await db() - .select({ - id: videos.id, - ownerId: videos.ownerId, - name: videos.name, - createdAt: videos.createdAt, - metadata: videos.metadata, - totalComments: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'text' THEN ${comments.id} END)`, - totalReactions: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'emoji' THEN ${comments.id} END)`, - ownerName: users.name, - folderName: sql`CASE WHEN ${folders.spaceId} = ${spaceId} THEN ${folders.name} ELSE NULL END`, - folderColor: sql`CASE WHEN ${folders.spaceId} = ${spaceId} THEN ${folders.color} ELSE NULL END`, - effectiveDate: sql` + const selectFields = { + id: videos.id, + ownerId: videos.ownerId, + name: videos.name, + createdAt: videos.createdAt, + metadata: videos.metadata, + totalComments: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'text' THEN ${comments.id} END)`, + totalReactions: sql`COUNT(DISTINCT CASE WHEN ${comments.type} = 'emoji' THEN ${comments.id} END)`, + ownerName: users.name, + folderName: folders.name, + folderColor: folders.color, + effectiveDate: sql` COALESCE( JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), ${videos.createdAt} ) `, - hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith( - Boolean, - ), - }) - .from(videos) - .leftJoin(comments, eq(videos.id, comments.videoId)) - .leftJoin(users, eq(videos.ownerId, users.id)) - .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) - .leftJoin(folders, eq(videos.folderId, folders.id)) - .leftJoin(spaces, eq(folders.spaceId, spaces.id)) - .where(eq(videos.ownerId, userId)) - .groupBy( - videos.id, - videos.ownerId, - videos.name, - videos.createdAt, - videos.metadata, - users.name, - folders.name, - folders.color, - folders.spaceId, - videos.folderId, - ) - .orderBy( - desc(sql`COALESCE( + hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith( + Boolean, + ), + }; + + const videoData = isAllSpacesEntry + ? await db() + .select(selectFields) + .from(videos) + .leftJoin(comments, eq(videos.id, comments.videoId)) + .leftJoin(users, eq(videos.ownerId, users.id)) + .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) + .leftJoin( + sharedVideos, + and( + eq(videos.id, sharedVideos.videoId), + eq(sharedVideos.organizationId, spaceId), + ), + ) + .leftJoin(folders, eq(sharedVideos.folderId, folders.id)) + .leftJoin(spaces, eq(folders.spaceId, spaces.id)) + .where(eq(videos.ownerId, userId)) + .groupBy( + videos.id, + videos.ownerId, + videos.name, + videos.createdAt, + videos.metadata, + users.name, + folders.name, + folders.color, + folders.spaceId, + videos.folderId, + ) + .orderBy( + desc(sql`COALESCE( + JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), + ${videos.createdAt} + )`), + ) + : await db() + .select(selectFields) + .from(videos) + .leftJoin(comments, eq(videos.id, comments.videoId)) + .leftJoin(users, eq(videos.ownerId, users.id)) + .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId)) + .leftJoin( + spaceVideos, + and( + eq(videos.id, spaceVideos.videoId), + eq(spaceVideos.spaceId, spaceId), + ), + ) + .leftJoin(folders, eq(spaceVideos.folderId, folders.id)) + .leftJoin(spaces, eq(folders.spaceId, spaces.id)) + .where(eq(videos.ownerId, userId)) + .groupBy( + videos.id, + videos.ownerId, + videos.name, + videos.createdAt, + videos.metadata, + users.name, + folders.name, + folders.color, + folders.spaceId, + videos.folderId, + ) + .orderBy( + desc(sql`COALESCE( JSON_UNQUOTE(JSON_EXTRACT(${videos.metadata}, '$.customCreatedAt')), ${videos.createdAt} )`), - ); + ); const processedVideoData = videoData.map((video) => { const { effectiveDate: _effectiveDate, ...videoWithoutEffectiveDate } = diff --git a/apps/web/app/(org)/dashboard/caps/page.tsx b/apps/web/app/(org)/dashboard/caps/page.tsx index ad73d21aa1..f78bb69d82 100644 --- a/apps/web/app/(org)/dashboard/caps/page.tsx +++ b/apps/web/app/(org)/dashboard/caps/page.tsx @@ -133,20 +133,6 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) { .where(eq(organizations.id, user.activeOrganizationId)) .limit(1); - let customDomain: string | null = null; - let domainVerified = false; - - if ( - organizationData.length > 0 && - organizationData[0] && - organizationData[0].customDomain - ) { - customDomain = organizationData[0].customDomain; - if (organizationData[0].domainVerified !== null) { - domainVerified = true; - } - } - const videoData = await db() .select({ id: videos.id, diff --git a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx index ae33089287..8fcbd4afc7 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx @@ -2,7 +2,6 @@ import type { Video } from "@cap/web-domain"; import { useQuery } from "@tanstack/react-query"; -import { useStore } from "@tanstack/react-store"; import { Effect, Exit } from "effect"; import { useRouter } from "next/navigation"; import { useMemo, useRef, useState } from "react"; diff --git a/apps/web/app/(org)/dashboard/folder/[id]/page.tsx b/apps/web/app/(org)/dashboard/folder/[id]/page.tsx index 302be68ece..43c6cb999b 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/page.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/page.tsx @@ -26,7 +26,9 @@ const FolderPage = async ({ params }: { params: { id: Folder.FolderId } }) => { const [childFolders, breadcrumb, videosData] = yield* Effect.all([ getChildFolders(params.id, { variant: "user" }), getFolderBreadcrumb(params.id), - getVideosByFolderId(params.id), + getVideosByFolderId(params.id, { + variant: "user", + }), ]); return ( diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index e53bf4079b..b116ce8cd3 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -58,7 +58,7 @@ export const SharedCaps = ({ count: number; dubApiKeyEnabled: boolean; spaceData?: SpaceData; - spaceId: string; + spaceId: Space.SpaceIdOrOrganisationId; hideSharedWith?: boolean; spaceMembers?: SpaceMemberData[]; organizationMembers?: OrganizationMemberData[]; @@ -182,7 +182,7 @@ export const SharedCaps = ({ setIsAddVideosDialogOpen(false)} - spaceId={spaceData.id} + spaceId={spaceId} spaceName={spaceData.name} onVideosAdded={handleVideosAdded} /> @@ -249,7 +249,7 @@ export const SharedCaps = ({ setIsAddVideosDialogOpen(false)} - spaceId={spaceData.id} + spaceId={spaceId} spaceName={spaceData.name} onVideosAdded={handleVideosAdded} /> diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx index 068861fa55..0affc903b1 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx @@ -161,7 +161,11 @@ const VideoCard: React.FC = memo( {video.folderName && (
- +

{video.folderName}

diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx index ac332ea251..a35fc8d991 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx @@ -41,7 +41,12 @@ const FolderPage = async (props: { : { variant: "org", organizationId: spaceOrOrg.organization.id }, ), getFolderBreadcrumb(params.folderId), - getVideosByFolderId(params.folderId), + getVideosByFolderId( + params.folderId, + spaceOrOrg.variant === "space" + ? { variant: "space", spaceId: spaceOrOrg.space.id } + : { variant: "org", organizationId: spaceOrOrg.organization.id }, + ), ]); return ( diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx index 846c09f65c..750d527067 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx @@ -35,7 +35,11 @@ export type SpaceMemberData = { }; // --- Helper functions --- -async function fetchFolders(spaceId: Space.SpaceIdOrOrganisationId) { +async function fetchFolders( + spaceId: Space.SpaceIdOrOrganisationId, + allSpacesEntry: boolean, +) { + const table = allSpacesEntry ? sharedVideos : spaceVideos; return db() .select({ id: folders.id, @@ -44,7 +48,7 @@ async function fetchFolders(spaceId: Space.SpaceIdOrOrganisationId) { parentId: folders.parentId, spaceId: folders.spaceId, videoCount: sql`( - SELECT COUNT(*) FROM videos WHERE videos.folderId = folders.id + SELECT COUNT(*) FROM ${table} WHERE ${table}.folderId = folders.id )`, }) .from(folders) @@ -110,7 +114,7 @@ export default async function SharedCapsPage(props: { await Promise.all([ fetchSpaceMembers(space.id), fetchOrganizationMembers(space.organizationId), - fetchFolders(space.id), + fetchFolders(space.id, false), ]); async function fetchSpaceVideos( @@ -195,7 +199,7 @@ export default async function SharedCapsPage(props: { data={processedVideoData} count={totalCount} spaceData={space} - spaceId={params.spaceId} + spaceId={params.spaceId as Space.SpaceIdOrOrganisationId} dubApiKeyEnabled={!!serverEnv().DUB_API_KEY} spaceMembers={spaceMembersData} organizationMembers={organizationMembersData} @@ -239,7 +243,7 @@ export default async function SharedCapsPage(props: { .where( and( eq(sharedVideos.organizationId, orgId), - isNull(videos.folderId), + isNull(sharedVideos.folderId), ), ) .groupBy( @@ -281,7 +285,7 @@ export default async function SharedCapsPage(props: { await Promise.all([ fetchOrganizationVideos(organization.id, page, limit), fetchOrganizationMembers(organization.id), - fetchFolders(organization.id), + fetchFolders(organization.id, true), ]); const { videos: orgVideoData, totalCount } = organizationVideos; @@ -303,7 +307,7 @@ export default async function SharedCapsPage(props: { count={totalCount} hideSharedWith organizationData={organization} - spaceId={params.spaceId} + spaceId={params.spaceId as Space.SpaceIdOrOrganisationId} dubApiKeyEnabled={!!serverEnv().DUB_API_KEY} organizationMembers={organizationMembersData} currentUserId={user.id} diff --git a/apps/web/lib/folder.ts b/apps/web/lib/folder.ts index 89b686d5a5..ed32731545 100644 --- a/apps/web/lib/folder.ts +++ b/apps/web/lib/folder.ts @@ -1,7 +1,5 @@ import "server-only"; -import { db } from "@cap/database"; -import { getCurrentUser } from "@cap/database/auth/session"; import { comments, folders, @@ -19,7 +17,6 @@ import { CurrentUser, Folder } from "@cap/web-domain"; import { and, desc, eq } 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"); @@ -159,6 +156,10 @@ const getSharedSpacesForVideos = Effect.fn(function* ( export const getVideosByFolderId = Effect.fn(function* ( folderId: Folder.FolderId, + root: + | { variant: "user" } + | { variant: "space"; spaceId: Space.SpaceIdOrOrganisationId } + | { variant: "org"; organizationId: Organisation.OrganisationId }, ) { if (!folderId) throw new Error("Folder ID is required"); const db = yield* Database; @@ -205,13 +206,20 @@ export const getVideosByFolderId = Effect.fn(function* ( .from(videos) .leftJoin(comments, eq(videos.id, comments.videoId)) .leftJoin(sharedVideos, eq(videos.id, sharedVideos.videoId)) + .leftJoin(spaceVideos, eq(videos.id, spaceVideos.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)) + .where( + root.variant === "space" + ? eq(spaceVideos.folderId, folderId) + : root.variant === "org" + ? eq(sharedVideos.folderId, folderId) + : eq(videos.folderId, folderId), + ) .groupBy( videos.id, videos.ownerId, diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index f3efe3fbdc..b8902b1995 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -1,69 +1,76 @@ { - "version": "5", - "dialect": "mysql", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1743020179593, - "tag": "0000_brown_sunfire", - "breakpoints": true - }, - { - "idx": 1, - "version": "5", - "when": 1749268354138, - "tag": "0001_white_young_avengers", - "breakpoints": true - }, - { - "idx": 2, - "version": "5", - "when": 1750935538683, - "tag": "0002_dusty_maginty", - "breakpoints": true - }, - { - "idx": 3, - "version": "5", - "when": 1751274435418, - "tag": "0003_outstanding_kylun", - "breakpoints": true - }, - { - "idx": 4, - "version": "5", - "when": 1751299325634, - "tag": "0004_optimal_eddie_brock", - "breakpoints": true - }, - { - "idx": 5, - "version": "5", - "when": 1751979972128, - "tag": "0005_graceful_fenris", - "breakpoints": true - }, - { - "idx": 6, - "version": "5", - "when": 1751982995648, - "tag": "0006_woozy_jamie_braddock", - "breakpoints": true - }, - { - "idx": 7, - "version": "5", - "when": 1754314124918, - "tag": "0007_cheerful_rocket_raccoon", - "breakpoints": true - }, - { - "idx": 8, - "version": "5", - "when": 1759139970377, - "tag": "0008_condemned_gamora", - "breakpoints": true - } - ] -} + "version": "5", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1743020179593, + "tag": "0000_brown_sunfire", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1749268354138, + "tag": "0001_white_young_avengers", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1750935538683, + "tag": "0002_dusty_maginty", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1751274435418, + "tag": "0003_outstanding_kylun", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1751299325634, + "tag": "0004_optimal_eddie_brock", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1751979972128, + "tag": "0005_graceful_fenris", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1751982995648, + "tag": "0006_woozy_jamie_braddock", + "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1754314124918, + "tag": "0007_cheerful_rocket_raccoon", + "breakpoints": true + }, + { + "idx": 8, + "version": "5", + "when": 1759139970377, + "tag": "0008_condemned_gamora", + "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1759993551600, + "tag": "0009_sad_carmella_unuscione", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/database/schema.ts b/packages/database/schema.ts index bac98ae965..11d29c3e9b 100644 --- a/packages/database/schema.ts +++ b/packages/database/schema.ts @@ -336,6 +336,7 @@ export const sharedVideos = mysqlTable( { id: nanoId("id").notNull().primaryKey().unique(), videoId: nanoId("videoId").notNull().$type(), + folderId: nanoIdNullable("folderId").$type(), organizationId: nanoId("organizationId") .notNull() .$type(), @@ -344,6 +345,7 @@ export const sharedVideos = mysqlTable( }, (table) => ({ videoIdIndex: index("video_id_idx").on(table.videoId), + folderIdIndex: index("folder_id_idx").on(table.folderId), organizationIdIndex: index("organization_id_idx").on(table.organizationId), sharedByUserIdIndex: index("shared_by_user_id_idx").on( table.sharedByUserId, @@ -352,6 +354,10 @@ export const sharedVideos = mysqlTable( table.videoId, table.organizationId, ), + videoIdFolderIdIndex: index("video_id_folder_id_idx").on( + table.videoId, + table.folderId, + ), }), ); From 4fb54eb6bcfe1f41cd3599f42a3ec1a43f953079 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:42:42 +0300 Subject: [PATCH 05/12] adjust height --- .../(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx | 2 +- .../spaces/[spaceId]/components/VirtualizedVideoGrid.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx index 0affc903b1..d8943ef290 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx @@ -61,7 +61,7 @@ const VideoCard: React.FC = memo( } }} className={clsx( - "flex relative flex-col p-3 w-full min-h-fit rounded-xl border transition-all duration-200 group", + "flex relative flex-col p-3 w-full h-full rounded-xl border transition-all duration-200 group", className, isAlreadyInEntity && isSelected && "border-red-500", isAlreadyInEntity && !isSelected && "border-blue-500", diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VirtualizedVideoGrid.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VirtualizedVideoGrid.tsx index e991bb89f5..e4c688a404 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VirtualizedVideoGrid.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VirtualizedVideoGrid.tsx @@ -83,7 +83,7 @@ const VirtualizedVideoGrid = ({ return (
Date: Thu, 9 Oct 2025 14:40:48 +0300 Subject: [PATCH 06/12] fixes --- apps/web/actions/folders/add-videos.ts | 46 ++++---- apps/web/actions/folders/get-folder-videos.ts | 24 +++-- apps/web/actions/folders/moveVideoToFolder.ts | 16 ++- apps/web/actions/folders/remove-videos.ts | 33 ++++-- apps/web/actions/organizations/add-videos.ts | 46 ++++---- .../organizations/get-organization-videos.ts | 9 +- apps/web/actions/spaces/add-videos.ts | 102 ++++++++++++++++-- apps/web/actions/spaces/get-space-videos.ts | 32 ++++-- apps/web/actions/spaces/remove-videos.ts | 92 +++++++++++----- .../[spaceId]/components/AddVideosDialog.tsx | 2 +- .../components/AddVideosDialogBase.tsx | 4 +- .../AddVideosToOrganizationDialog.tsx | 13 +-- .../spaces/[spaceId]/components/VideoCard.tsx | 44 +++++--- .../folder/[folderId]/AddVideosButton.tsx | 10 +- .../[spaceId]/folder/[folderId]/page.tsx | 5 +- 15 files changed, 340 insertions(+), 138 deletions(-) diff --git a/apps/web/actions/folders/add-videos.ts b/apps/web/actions/folders/add-videos.ts index 55c0a9c39a..2c95af5847 100644 --- a/apps/web/actions/folders/add-videos.ts +++ b/apps/web/actions/folders/add-videos.ts @@ -2,7 +2,12 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { folders, spaceVideos, videos } from "@cap/database/schema"; +import { + folders, + sharedVideos, + spaceVideos, + videos, +} from "@cap/database/schema"; import type { Folder, Space, Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; @@ -10,7 +15,7 @@ import { revalidatePath } from "next/cache"; export async function addVideosToFolder( folderId: Folder.FolderId, videoIds: Video.VideoId[], - spaceId?: string, + spaceId: Space.SpaceIdOrOrganisationId, ) { try { const user = await getCurrentUser(); @@ -23,7 +28,6 @@ export async function addVideosToFolder( throw new Error("Missing required data"); } - // Verify folder exists and is accessible const [folder] = await db() .select({ id: folders.id, spaceId: folders.spaceId }) .from(folders) @@ -33,7 +37,6 @@ export async function addVideosToFolder( throw new Error("Folder not found"); } - // Only allow updating videos the user owns const userVideos = await db() .select({ id: videos.id }) .from(videos) @@ -45,38 +48,35 @@ export async function addVideosToFolder( throw new Error("No valid videos found"); } - // Update the video's folderId - await db() - .update(videos) - .set({ folderId: folderId, updatedAt: new Date() }) - .where(inArray(videos.id, validVideoIds)); + const isAllSpacesEntry = spaceId === user.activeOrganizationId; - // If this folder belongs to a space and we have a spaceId context, update spaceVideos relation - const effectiveSpaceId = spaceId || folder.spaceId; - if (effectiveSpaceId) { - // Update folderId for videos that are already in this space + //if video already exists in the space, then move it + if (isAllSpacesEntry) { + await db() + .update(sharedVideos) + .set({ folderId }) + .where( + and( + eq(sharedVideos.organizationId, user.activeOrganizationId), + inArray(sharedVideos.videoId, validVideoIds), + ), + ); + } else { await db() .update(spaceVideos) .set({ folderId }) .where( and( - eq( - spaceVideos.spaceId, - effectiveSpaceId as Space.SpaceIdOrOrganisationId, - ), + eq(spaceVideos.spaceId, spaceId), inArray(spaceVideos.videoId, validVideoIds), ), ); } - // Revalidate relevant paths revalidatePath(`/dashboard/caps`); revalidatePath(`/dashboard/folder/${folderId}`); - const effectiveSpaceIdForRevalidate = spaceId || folder.spaceId; - if (effectiveSpaceIdForRevalidate) { - revalidatePath( - `/dashboard/spaces/${effectiveSpaceIdForRevalidate}/folder/${folderId}`, - ); + if (spaceId) { + revalidatePath(`/dashboard/spaces/${spaceId}/folder/${folderId}`); } return { diff --git a/apps/web/actions/folders/get-folder-videos.ts b/apps/web/actions/folders/get-folder-videos.ts index 7002d44d0e..704f6aeb22 100644 --- a/apps/web/actions/folders/get-folder-videos.ts +++ b/apps/web/actions/folders/get-folder-videos.ts @@ -2,11 +2,14 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { videos } from "@cap/database/schema"; -import type { Folder, Video } from "@cap/web-domain"; +import { sharedVideos, spaceVideos } from "@cap/database/schema"; +import type { Folder, Space, Video } from "@cap/web-domain"; import { eq } from "drizzle-orm"; -export async function getFolderVideoIds(folderId: Folder.FolderId) { +export async function getFolderVideoIds( + folderId: Folder.FolderId, + spaceId: Space.SpaceIdOrOrganisationId, +) { try { const user = await getCurrentUser(); @@ -18,10 +21,17 @@ export async function getFolderVideoIds(folderId: Folder.FolderId) { throw new Error("Folder ID is required"); } - const rows = await db() - .select({ id: videos.id }) - .from(videos) - .where(eq(videos.folderId, folderId)); + const isAllSpacesEntry = user.activeOrganizationId === spaceId; + + const rows = isAllSpacesEntry + ? await db() + .select({ id: sharedVideos.videoId }) + .from(sharedVideos) + .where(eq(sharedVideos.folderId, folderId)) + : await db() + .select({ id: spaceVideos.videoId }) + .from(spaceVideos) + .where(eq(spaceVideos.folderId, folderId)); return { success: true, diff --git a/apps/web/actions/folders/moveVideoToFolder.ts b/apps/web/actions/folders/moveVideoToFolder.ts index 69c4266db1..34f1f39980 100644 --- a/apps/web/actions/folders/moveVideoToFolder.ts +++ b/apps/web/actions/folders/moveVideoToFolder.ts @@ -8,10 +8,9 @@ import { spaceVideos, videos, } from "@cap/database/schema"; -import type { Folder, Video } from "@cap/web-domain"; +import type { Folder, Space, Video } from "@cap/web-domain"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; - export async function moveVideoToFolder({ videoId, folderId, @@ -19,7 +18,7 @@ export async function moveVideoToFolder({ }: { videoId: Video.VideoId; folderId: Folder.FolderId | null; - spaceId?: string | null; + spaceId: Space.SpaceIdOrOrganisationId; }) { const user = await getCurrentUser(); if (!user || !user.activeOrganizationId) @@ -60,14 +59,21 @@ export async function moveVideoToFolder({ .set({ folderId: folderId === null ? null : folderId, }) - .where(eq(spaceVideos.videoId, videoId)); + .where( + and(eq(spaceVideos.videoId, videoId), eq(spaceVideos.spaceId, spaceId)), + ); } else if (spaceId && isAllSpacesEntry) { await db() .update(sharedVideos) .set({ folderId: folderId === null ? null : folderId, }) - .where(eq(sharedVideos.videoId, videoId)); + .where( + and( + eq(sharedVideos.videoId, videoId), + eq(sharedVideos.organizationId, user.activeOrganizationId), + ), + ); } else { await db() .update(videos) diff --git a/apps/web/actions/folders/remove-videos.ts b/apps/web/actions/folders/remove-videos.ts index eb372bb7cb..680a7a3590 100644 --- a/apps/web/actions/folders/remove-videos.ts +++ b/apps/web/actions/folders/remove-videos.ts @@ -2,14 +2,20 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { folders, spaceVideos, videos } from "@cap/database/schema"; -import type { Folder, Video } from "@cap/web-domain"; -import { and, eq, inArray } from "drizzle-orm"; +import { + folders, + sharedVideos, + spaceVideos, + videos, +} from "@cap/database/schema"; +import type { Folder, Space, Video } from "@cap/web-domain"; +import { and, eq, inArray, sql } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function removeVideosFromFolder( folderId: Folder.FolderId, videoIds: Video.VideoId[], + spaceId: Space.SpaceIdOrOrganisationId, ) { try { const user = await getCurrentUser(); @@ -18,6 +24,8 @@ export async function removeVideosFromFolder( throw new Error("Unauthorized"); } + const isAllSpacesEntry = user.activeOrganizationId === spaceId; + if (!folderId || !videoIds || videoIds.length === 0) { throw new Error("Missing required data"); } @@ -52,14 +60,27 @@ export async function removeVideosFromFolder( and(inArray(videos.id, validVideoIds), eq(videos.folderId, folderId)), ); - // If folder belongs to a space, also clear the folderId in spaceVideos relation - if (folder.spaceId) { + // Clear the folderId in the appropriate table based on context + if (isAllSpacesEntry || !folder.spaceId) { + // Organization-level folder - clear folderId in sharedVideos + await db() + .update(sharedVideos) + .set({ folderId: null }) + .where( + and( + eq(sharedVideos.organizationId, user.activeOrganizationId), + inArray(sharedVideos.videoId, validVideoIds), + eq(sharedVideos.folderId, folderId), + ), + ); + } else if (folder.spaceId) { + // Space-level folder - clear folderId in spaceVideos await db() .update(spaceVideos) .set({ folderId: null }) .where( and( - eq(spaceVideos.spaceId, folder.spaceId), + sql`${spaceVideos.spaceId} = ${folder.spaceId}`, inArray(spaceVideos.videoId, validVideoIds), eq(spaceVideos.folderId, folderId), ), diff --git a/apps/web/actions/organizations/add-videos.ts b/apps/web/actions/organizations/add-videos.ts index 97c24fc84e..a811dcd623 100644 --- a/apps/web/actions/organizations/add-videos.ts +++ b/apps/web/actions/organizations/add-videos.ts @@ -87,36 +87,40 @@ export async function addVideosToOrganization( (id) => !existingVideoIds.includes(id), ); - if (newVideoIds.length === 0) { - return { - success: true, - message: "Videos already shared with organization", - }; + // Update existing videos to move them to root (clear folderId) + if (existingVideoIds.length > 0) { + await db() + .update(sharedVideos) + .set({ folderId: null }) + .where( + and( + eq(sharedVideos.organizationId, organizationId), + inArray(sharedVideos.videoId, existingVideoIds), + ), + ); } - const sharedVideoEntries = newVideoIds.map((videoId) => ({ - id: nanoId(), - videoId, - organizationId, - sharedByUserId: user.id, - })); + // Insert new videos + if (newVideoIds.length > 0) { + const sharedVideoEntries = newVideoIds.map((videoId) => ({ + id: nanoId(), + videoId, + organizationId, + sharedByUserId: user.id, + })); - await db().insert(sharedVideos).values(sharedVideoEntries); - - // Clear folderId for videos added to organization so they appear in main view - await db() - .update(videos) - .set({ folderId: null }) - .where(inArray(videos.id, newVideoIds)); + await db().insert(sharedVideos).values(sharedVideoEntries); + } revalidatePath(`/dashboard/spaces/${organizationId}`); revalidatePath("/dashboard/caps"); + const totalUpdated = existingVideoIds.length + newVideoIds.length; return { success: true, - message: `${newVideoIds.length} video${ - newVideoIds.length === 1 ? "" : "s" - } shared with organization`, + message: `${totalUpdated} video${ + totalUpdated === 1 ? "" : "s" + } ${totalUpdated === 1 ? "is" : "are"} now in organization root`, }; } catch (error) { console.error("Error adding videos to organization:", error); diff --git a/apps/web/actions/organizations/get-organization-videos.ts b/apps/web/actions/organizations/get-organization-videos.ts index 41431c3af1..2e584388f4 100644 --- a/apps/web/actions/organizations/get-organization-videos.ts +++ b/apps/web/actions/organizations/get-organization-videos.ts @@ -4,7 +4,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { sharedVideos } from "@cap/database/schema"; import type { Organisation } from "@cap/web-domain"; -import { eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; export async function getOrganizationVideoIds( organizationId: Organisation.OrganisationId, @@ -25,7 +25,12 @@ export async function getOrganizationVideoIds( videoId: sharedVideos.videoId, }) .from(sharedVideos) - .where(eq(sharedVideos.organizationId, organizationId)); + .where( + and( + eq(sharedVideos.organizationId, organizationId), + isNull(sharedVideos.folderId), + ), + ); return { success: true, diff --git a/apps/web/actions/spaces/add-videos.ts b/apps/web/actions/spaces/add-videos.ts index ad19d18d46..efa3f50850 100644 --- a/apps/web/actions/spaces/add-videos.ts +++ b/apps/web/actions/spaces/add-videos.ts @@ -3,7 +3,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoId } from "@cap/database/helpers"; -import { spaceVideos, videos } from "@cap/database/schema"; +import { sharedVideos, spaceVideos, videos } from "@cap/database/schema"; import type { Space, Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; @@ -23,6 +23,8 @@ export async function addVideosToSpace( throw new Error("Missing required data"); } + const isAllSpacesEntry = user.activeOrganizationId === spaceId; + const userVideos = await db() .select({ id: videos.id }) .from(videos) @@ -34,21 +36,103 @@ export async function addVideosToSpace( throw new Error("No valid videos found"); } - const spaceVideoEntries = validVideoIds.map((videoId) => ({ - id: nanoId(), - videoId, - spaceId, - addedById: user.id, - })); + if (isAllSpacesEntry) { + console.log({ + validVideoIds, + }); + + // Check which videos already exist in sharedVideos + const existingSharedVideos = await db() + .select({ videoId: sharedVideos.videoId }) + .from(sharedVideos) + .where( + and( + eq(sharedVideos.organizationId, spaceId), + inArray(sharedVideos.videoId, validVideoIds), + ), + ); + + console.log({ + existingSharedVideos, + }); + + const existingVideoIds = existingSharedVideos.map((v) => v.videoId); + const newVideoIds = validVideoIds.filter( + (id) => !existingVideoIds.includes(id), + ); + + // Update existing videos to move them to root (clear folderId) + if (existingVideoIds.length > 0) { + await db() + .update(sharedVideos) + .set({ folderId: null }) + .where( + and( + eq(sharedVideos.organizationId, spaceId), + inArray(sharedVideos.videoId, existingVideoIds), + ), + ); + } - await db().insert(spaceVideos).values(spaceVideoEntries); + // Insert new videos + if (newVideoIds.length > 0) { + const sharedVideoEntries = newVideoIds.map((videoId) => ({ + id: nanoId(), + videoId, + organizationId: spaceId, + sharedByUserId: user.id, + })); + await db().insert(sharedVideos).values(sharedVideoEntries); + } + } else { + // Check which videos already exist in spaceVideos + const existingSpaceVideos = await db() + .select({ videoId: spaceVideos.videoId }) + .from(spaceVideos) + .where( + and( + eq(spaceVideos.spaceId, spaceId), + inArray(spaceVideos.videoId, validVideoIds), + ), + ); + + const existingVideoIds = existingSpaceVideos.map((v) => v.videoId); + const newVideoIds = validVideoIds.filter( + (id) => !existingVideoIds.includes(id), + ); + + // Update existing videos to move them to root (clear folderId) + if (existingVideoIds.length > 0) { + await db() + .update(spaceVideos) + .set({ folderId: null }) + .where( + and( + eq(spaceVideos.spaceId, spaceId), + inArray(spaceVideos.videoId, existingVideoIds), + ), + ); + } + + // Insert new videos + if (newVideoIds.length > 0) { + const spaceVideoEntries = newVideoIds.map((videoId) => ({ + id: nanoId(), + videoId, + spaceId, + addedById: user.id, + })); + + await db().insert(spaceVideos).values(spaceVideoEntries); + } + } revalidatePath(`/dashboard/spaces/${spaceId}`); revalidatePath("/dashboard/caps"); return { success: true, - message: `${validVideoIds.length} video${validVideoIds.length === 1 ? "" : "s"} added to space`, + message: `${validVideoIds.length} video${validVideoIds.length === 1 ? "" : "s"} added to ${isAllSpacesEntry ? "organization" : "space"}`, }; } catch (error) { console.error("Error adding videos to space:", error); diff --git a/apps/web/actions/spaces/get-space-videos.ts b/apps/web/actions/spaces/get-space-videos.ts index 78653404da..ddbcc42b82 100644 --- a/apps/web/actions/spaces/get-space-videos.ts +++ b/apps/web/actions/spaces/get-space-videos.ts @@ -2,7 +2,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { spaceVideos } from "@cap/database/schema"; +import { sharedVideos, spaceVideos } from "@cap/database/schema"; import type { Space } from "@cap/web-domain"; import { and, eq, isNull } from "drizzle-orm"; @@ -18,14 +18,28 @@ export async function getSpaceVideoIds(spaceId: Space.SpaceIdOrOrganisationId) { throw new Error("Space ID is required"); } - const videoIds = await db() - .select({ - videoId: spaceVideos.videoId, - }) - .from(spaceVideos) - .where( - and(eq(spaceVideos.spaceId, spaceId), isNull(spaceVideos.folderId)), - ); + const isAllSpacesEntry = user.activeOrganizationId === spaceId; + + const videoIds = isAllSpacesEntry + ? await db() + .select({ + videoId: sharedVideos.videoId, + }) + .from(sharedVideos) + .where( + and( + eq(sharedVideos.organizationId, spaceId), + isNull(sharedVideos.folderId), + ), + ) + : await db() + .select({ + videoId: spaceVideos.videoId, + }) + .from(spaceVideos) + .where( + and(eq(spaceVideos.spaceId, spaceId), isNull(spaceVideos.folderId)), + ); return { success: true, diff --git a/apps/web/actions/spaces/remove-videos.ts b/apps/web/actions/spaces/remove-videos.ts index dcf39fb264..b569ee440e 100644 --- a/apps/web/actions/spaces/remove-videos.ts +++ b/apps/web/actions/spaces/remove-videos.ts @@ -2,9 +2,14 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { folders, spaceVideos, videos } from "@cap/database/schema"; +import { + folders, + sharedVideos, + spaceVideos, + videos, +} from "@cap/database/schema"; import type { Space, Video } from "@cap/web-domain"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, isNull } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function removeVideosFromSpace( @@ -34,47 +39,82 @@ export async function removeVideosFromSpace( throw new Error("No valid videos found"); } - // Remove from spaceVideos - await db() - .delete(spaceVideos) - .where( - and( - eq(spaceVideos.spaceId, spaceId), - inArray(spaceVideos.videoId, validVideoIds), - ), - ); + const isAllSpacesEntry = user.activeOrganizationId === spaceId; - // Set folderId to null for any removed videos that are in folders belonging to this space - // Find all folder IDs in this space - const folderRows = await db() - .select({ id: folders.id }) - .from(folders) - .where(eq(folders.spaceId, spaceId)); - const folderIds = folderRows.map((f) => f.id); + if (isAllSpacesEntry) { + // Remove from organization level (sharedVideos table) + await db() + .delete(sharedVideos) + .where( + and( + eq(sharedVideos.organizationId, spaceId), + inArray(sharedVideos.videoId, validVideoIds), + ), + ); + + // Set folderId to null for any removed videos that are in org-level folders + const folderRows = await db() + .select({ id: folders.id }) + .from(folders) + .where(isNull(folders.spaceId)); + const folderIds = folderRows.map((f) => f.id); - if (folderIds.length > 0) { + if (folderIds.length > 0) { + await db() + .update(videos) + .set({ folderId: null }) + .where( + and( + inArray(videos.id, validVideoIds), + inArray(videos.folderId, folderIds), + ), + ); + } + } else { + // Remove from specific space (spaceVideos table) await db() - .update(videos) - .set({ folderId: null }) + .delete(spaceVideos) .where( and( - inArray(videos.id, validVideoIds), - inArray(videos.folderId, folderIds), + eq(spaceVideos.spaceId, spaceId), + inArray(spaceVideos.videoId, validVideoIds), ), ); + + // Set folderId to null for any removed videos that are in folders belonging to this space + const folderRows = await db() + .select({ id: folders.id }) + .from(folders) + .where(eq(folders.spaceId, spaceId)); + const folderIds = folderRows.map((f) => f.id); + + if (folderIds.length > 0) { + await db() + .update(videos) + .set({ folderId: null }) + .where( + and( + inArray(videos.id, validVideoIds), + inArray(videos.folderId, folderIds), + ), + ); + } } revalidatePath(`/dashboard/spaces/${spaceId}`); return { success: true, - message: `Removed ${validVideoIds.length} video(s) from space and folders`, + message: `Removed ${validVideoIds.length} video(s) from ${isAllSpacesEntry ? "organization" : "space"} and folders`, deletedCount: validVideoIds.length, }; - } catch (error: any) { + } catch (error) { return { success: false, - message: error.message || "Failed to remove videos from space", + message: + error instanceof Error + ? error.message + : "Failed to remove videos from space", }; } } diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx index 28ab284cf5..027f2083c7 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx @@ -33,7 +33,7 @@ export const AddVideosDialog: React.FC = ({ addVideos={addVideosToSpace} removeVideos={removeVideosFromSpace} getVideos={() => getUserVideos(spaceId)} - getEntityVideoIds={getSpaceVideoIds} + getEntityVideoIds={() => getSpaceVideoIds(spaceId)} /> ); }; diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx index eb0bc64dfd..9ab333aeb1 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx @@ -31,7 +31,7 @@ interface AddVideosDialogBaseProp { addVideos: (entityId: T, videoIds: Video.VideoId[]) => Promise; removeVideos: (entityId: T, videoIds: Video.VideoId[]) => Promise; getVideos: () => Promise; - getEntityVideoIds: (entityId: T) => Promise; + getEntityVideoIds: () => Promise; } export interface VideoData { @@ -94,7 +94,7 @@ function AddVideosDialogBase({ const { data: entityVideoIds } = useQuery({ queryKey: ["entity-video-ids", entityId, entityName], queryFn: async () => { - const result = await getEntityVideoIds(entityId); + const result = await getEntityVideoIds(); if (!result.success) { throw new Error(result.error); } diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx index 4858daff92..5810e08046 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosToOrganizationDialog.tsx @@ -19,14 +19,7 @@ interface AddVideosToOrganizationDialogProps { export const AddVideosToOrganizationDialog: React.FC< AddVideosToOrganizationDialogProps -> = ({ - open, - onClose, - organizationId, - organizationName, - onVideosAdded, - spaceId, -}) => { +> = ({ open, onClose, organizationId, organizationName, onVideosAdded }) => { return ( getUserVideos(spaceId)} - getEntityVideoIds={getOrganizationVideoIds} + getVideos={() => getUserVideos(organizationId)} + getEntityVideoIds={() => getOrganizationVideoIds(organizationId)} /> ); }; diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx index d8943ef290..884508aca3 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/VideoCard.tsx @@ -1,3 +1,5 @@ +import { faHome, faRecordVinyl } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Fit, Layout, useRive } from "@rive-app/react-canvas"; import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; @@ -159,18 +161,36 @@ const VideoCard: React.FC = memo( {moment(effectiveDate).format("MMM D, YYYY")}

- {video.folderName && ( -
- -

- {video.folderName} -

-
- )} +
+ {video.folderName ? ( + <> + +

+ {video.folderName} +

+ + ) : isAlreadyInEntity ? ( + <> + +

Root

+ + ) : ( + <> + +

Caps

+ + )} +
); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx index c684a177ba..415d018574 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/AddVideosButton.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@cap/ui"; -import type { Folder } from "@cap/web-domain"; +import type { Folder, Space } from "@cap/web-domain"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useRouter } from "next/navigation"; @@ -19,7 +19,7 @@ export default function AddVideosButton({ }: { folderId: Folder.FolderId; folderName: string; - spaceId: string; + spaceId: Space.SpaceIdOrOrganisationId; }) { const [open, setOpen] = useState(false); const router = useRouter(); @@ -41,9 +41,11 @@ export default function AddVideosButton({ addVideos={(folderIdArg, videoIds) => addVideosToFolder(folderIdArg, videoIds, spaceId) } - removeVideos={removeVideosFromFolder} + removeVideos={(folderIdArg, videoIds) => + removeVideosFromFolder(folderIdArg, videoIds, spaceId) + } getVideos={() => getUserVideos(spaceId)} - getEntityVideoIds={getFolderVideoIds} + getEntityVideoIds={() => getFolderVideoIds(folderId, spaceId)} /> ); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx index a35fc8d991..bb55cce82c 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx @@ -20,7 +20,10 @@ import FolderVideosSection from "../../../../folder/[id]/components/FolderVideos import AddVideosButton from "./AddVideosButton"; const FolderPage = async (props: { - params: Promise<{ spaceId: string; folderId: Folder.FolderId }>; + params: Promise<{ + spaceId: Space.SpaceIdOrOrganisationId; + folderId: Folder.FolderId; + }>; }) => { const params = await props.params; const user = await getCurrentUser(); From 1d2f118b5ffeefd698cf64aff39cf5fbe3bedb01 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:48:07 +0300 Subject: [PATCH 07/12] Update _journal.json --- .../database/migrations/meta/_journal.json | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index b8902b1995..d045a0c84c 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -1,76 +1,76 @@ { - "version": "5", - "dialect": "mysql", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1743020179593, - "tag": "0000_brown_sunfire", - "breakpoints": true - }, - { - "idx": 1, - "version": "5", - "when": 1749268354138, - "tag": "0001_white_young_avengers", - "breakpoints": true - }, - { - "idx": 2, - "version": "5", - "when": 1750935538683, - "tag": "0002_dusty_maginty", - "breakpoints": true - }, - { - "idx": 3, - "version": "5", - "when": 1751274435418, - "tag": "0003_outstanding_kylun", - "breakpoints": true - }, - { - "idx": 4, - "version": "5", - "when": 1751299325634, - "tag": "0004_optimal_eddie_brock", - "breakpoints": true - }, - { - "idx": 5, - "version": "5", - "when": 1751979972128, - "tag": "0005_graceful_fenris", - "breakpoints": true - }, - { - "idx": 6, - "version": "5", - "when": 1751982995648, - "tag": "0006_woozy_jamie_braddock", - "breakpoints": true - }, - { - "idx": 7, - "version": "5", - "when": 1754314124918, - "tag": "0007_cheerful_rocket_raccoon", - "breakpoints": true - }, - { - "idx": 8, - "version": "5", - "when": 1759139970377, - "tag": "0008_condemned_gamora", - "breakpoints": true - }, - { - "idx": 9, - "version": "5", - "when": 1759993551600, - "tag": "0009_sad_carmella_unuscione", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "5", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1743020179593, + "tag": "0000_brown_sunfire", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1749268354138, + "tag": "0001_white_young_avengers", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1750935538683, + "tag": "0002_dusty_maginty", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1751274435418, + "tag": "0003_outstanding_kylun", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1751299325634, + "tag": "0004_optimal_eddie_brock", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1751979972128, + "tag": "0005_graceful_fenris", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1751982995648, + "tag": "0006_woozy_jamie_braddock", + "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1754314124918, + "tag": "0007_cheerful_rocket_raccoon", + "breakpoints": true + }, + { + "idx": 8, + "version": "5", + "when": 1759139970377, + "tag": "0008_condemned_gamora", + "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1759993551600, + "tag": "0009_sad_carmella_unuscione", + "breakpoints": true + } + ] +} From 9c0ca54b484163a290f6e5f3562a6a5cdd3533cb Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:58:17 +0300 Subject: [PATCH 08/12] ts --- apps/web/actions/folders/moveVideoToFolder.ts | 2 +- .../(org)/dashboard/caps/components/Folder.tsx | 6 +++--- .../folder/[id]/components/BreadcrumbItem.tsx | 10 ++++++++-- .../[id]/components/ClientMyCapsLink.tsx | 18 ++++++++++-------- .../app/(org)/dashboard/folder/[id]/page.tsx | 12 +++++++++--- .../[spaceId]/folder/[folderId]/page.tsx | 3 ++- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/apps/web/actions/folders/moveVideoToFolder.ts b/apps/web/actions/folders/moveVideoToFolder.ts index 34f1f39980..16e65e7cdf 100644 --- a/apps/web/actions/folders/moveVideoToFolder.ts +++ b/apps/web/actions/folders/moveVideoToFolder.ts @@ -18,7 +18,7 @@ export async function moveVideoToFolder({ }: { videoId: Video.VideoId; folderId: Folder.FolderId | null; - spaceId: Space.SpaceIdOrOrganisationId; + spaceId: Space.SpaceIdOrOrganisationId | null; }) { const user = await getCurrentUser(); if (!user || !user.activeOrganizationId) diff --git a/apps/web/app/(org)/dashboard/caps/components/Folder.tsx b/apps/web/app/(org)/dashboard/caps/components/Folder.tsx index 51b2b846d1..5a2d0a8f8f 100644 --- a/apps/web/app/(org)/dashboard/caps/components/Folder.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/Folder.tsx @@ -1,5 +1,5 @@ "use client"; -import type { Folder } from "@cap/web-domain"; +import type { Folder, Space } from "@cap/web-domain"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Fit, Layout, useRive } from "@rive-app/react-canvas"; @@ -21,8 +21,8 @@ export type FolderDataType = { id: Folder.FolderId; color: "normal" | "blue" | "red" | "yellow"; videoCount: number; - spaceId?: string | null; - parentId?: string | null; + spaceId: Space.SpaceIdOrOrganisationId; + parentId: Folder.FolderId | null; }; const FolderCard = ({ diff --git a/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx b/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx index 30f486272c..05ede8208c 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx @@ -1,6 +1,6 @@ "use client"; -import type { Folder } from "@cap/web-domain"; +import type { Folder, Space } from "@cap/web-domain"; import clsx from "clsx"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -15,6 +15,7 @@ interface BreadcrumbItemProps { id: Folder.FolderId; name: string; color: "normal" | "blue" | "red" | "yellow"; + spaceId: Space.SpaceIdOrOrganisationId; isLast: boolean; } @@ -23,6 +24,7 @@ export function BreadcrumbItem({ name, color, isLast, + spaceId, }: BreadcrumbItemProps) { const [isDragOver, setIsDragOver] = useState(false); const [isMoving, setIsMoving] = useState(false); @@ -59,7 +61,11 @@ export function BreadcrumbItem({ if (!capData.id) return; setIsMoving(true); - await moveVideoToFolder({ videoId: capData.id, folderId: id }); + await moveVideoToFolder({ + videoId: capData.id, + folderId: id, + spaceId, + }); router.refresh(); toast.success(`"${capData.name}" moved to "${name}" folder`); } catch (error) { diff --git a/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx b/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx index 70ed6af15c..3e378acfdd 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx @@ -1,7 +1,7 @@ "use client"; import { Avatar } from "@cap/ui"; -import type { Video } from "@cap/web-domain"; +import type { Space, Video } from "@cap/web-domain"; import clsx from "clsx"; import Image from "next/image"; import Link from "next/link"; @@ -12,7 +12,11 @@ import { moveVideoToFolder } from "@/actions/folders/moveVideoToFolder"; import { useDashboardContext } from "../../../Contexts"; import { registerDropTarget } from "./ClientCapCard"; -export function ClientMyCapsLink() { +export function ClientMyCapsLink({ + spaceId, +}: { + spaceId: Space.SpaceIdOrOrganisationId; +}) { const [isDragOver, setIsDragOver] = useState(false); const [isMovingVideo, setIsMovingVideo] = useState(false); const linkRef = useRef(null); @@ -90,11 +94,11 @@ export function ClientMyCapsLink() { await moveVideoToFolder({ videoId: capData.id, folderId: null, - spaceId: activeSpace?.id, + spaceId, }); router.refresh(); - if (activeSpace) { - toast.success(`Moved "${capData.name}" to "${activeSpace.name}"`); + if (spaceId) { + toast.success(`Moved "${capData.name}" to "${spaceId}"`); } else { toast.success(`Moved "${capData.name}" to My Caps`); } @@ -109,9 +113,7 @@ export function ClientMyCapsLink() { return ( { +const FolderPage = async ({ + params, +}: { + params: { id: Folder.FolderId; spaceId: Space.SpaceIdOrOrganisationId }; +}) => { const user = await getCurrentUser(); if (!user || !user.activeOrganizationId) return notFound(); @@ -39,13 +43,14 @@ const FolderPage = async ({ params }: { params: { id: Folder.FolderId } }) => {
- + {breadcrumb.map((folder, index) => (

/

{ key={folder.id} name={folder.name} color={folder.color} + spaceId={params.spaceId} id={folder.id} parentId={folder.parentId} videoCount={folder.videoCount} diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx index bb55cce82c..655f78b9d5 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx @@ -64,11 +64,12 @@ const FolderPage = async (props: {
- + {breadcrumb.map((folder, index) => (

/

Date: Thu, 9 Oct 2025 15:15:01 +0300 Subject: [PATCH 09/12] ts --- apps/web/actions/folders/moveVideoToFolder.ts | 2 +- apps/web/actions/spaces/remove-videos.ts | 16 ++++++++++++++-- .../(org)/dashboard/caps/components/Folder.tsx | 2 +- .../folder/[id]/components/ClientMyCapsLink.tsx | 8 ++++---- .../web/app/(org)/dashboard/folder/[id]/page.tsx | 3 +-- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/web/actions/folders/moveVideoToFolder.ts b/apps/web/actions/folders/moveVideoToFolder.ts index 16e65e7cdf..59d8de51d0 100644 --- a/apps/web/actions/folders/moveVideoToFolder.ts +++ b/apps/web/actions/folders/moveVideoToFolder.ts @@ -18,7 +18,7 @@ export async function moveVideoToFolder({ }: { videoId: Video.VideoId; folderId: Folder.FolderId | null; - spaceId: Space.SpaceIdOrOrganisationId | null; + spaceId?: Space.SpaceIdOrOrganisationId | null; }) { const user = await getCurrentUser(); if (!user || !user.activeOrganizationId) diff --git a/apps/web/actions/spaces/remove-videos.ts b/apps/web/actions/spaces/remove-videos.ts index b569ee440e..1f4ea469c7 100644 --- a/apps/web/actions/spaces/remove-videos.ts +++ b/apps/web/actions/spaces/remove-videos.ts @@ -56,7 +56,13 @@ export async function removeVideosFromSpace( const folderRows = await db() .select({ id: folders.id }) .from(folders) - .where(isNull(folders.spaceId)); + .where( + and( + isNull(folders.spaceId), + eq(folders.organizationId, user.activeOrganizationId), + ), + ); + const folderIds = folderRows.map((f) => f.id); if (folderIds.length > 0) { @@ -85,7 +91,13 @@ export async function removeVideosFromSpace( const folderRows = await db() .select({ id: folders.id }) .from(folders) - .where(eq(folders.spaceId, spaceId)); + .where( + and( + isNull(folders.spaceId), + eq(folders.organizationId, user.activeOrganizationId), + ), + ); + const folderIds = folderRows.map((f) => f.id); if (folderIds.length > 0) { diff --git a/apps/web/app/(org)/dashboard/caps/components/Folder.tsx b/apps/web/app/(org)/dashboard/caps/components/Folder.tsx index 5a2d0a8f8f..239821e25a 100644 --- a/apps/web/app/(org)/dashboard/caps/components/Folder.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/Folder.tsx @@ -21,7 +21,7 @@ export type FolderDataType = { id: Folder.FolderId; color: "normal" | "blue" | "red" | "yellow"; videoCount: number; - spaceId: Space.SpaceIdOrOrganisationId; + spaceId?: Space.SpaceIdOrOrganisationId | null; parentId: Folder.FolderId | null; }; diff --git a/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx b/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx index 3e378acfdd..9d135f4c14 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/ClientMyCapsLink.tsx @@ -15,7 +15,7 @@ import { registerDropTarget } from "./ClientCapCard"; export function ClientMyCapsLink({ spaceId, }: { - spaceId: Space.SpaceIdOrOrganisationId; + spaceId?: Space.SpaceIdOrOrganisationId; }) { const [isDragOver, setIsDragOver] = useState(false); const [isMovingVideo, setIsMovingVideo] = useState(false); @@ -94,11 +94,11 @@ export function ClientMyCapsLink({ await moveVideoToFolder({ videoId: capData.id, folderId: null, - spaceId, + spaceId: spaceId ?? null, }); router.refresh(); - if (spaceId) { - toast.success(`Moved "${capData.name}" to "${spaceId}"`); + if (activeSpace) { + toast.success(`Moved "${capData.name}" to "${activeSpace.name}"`); } else { toast.success(`Moved "${capData.name}" to My Caps`); } diff --git a/apps/web/app/(org)/dashboard/folder/[id]/page.tsx b/apps/web/app/(org)/dashboard/folder/[id]/page.tsx index 1d051b0bdd..db74907d70 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/page.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/page.tsx @@ -43,7 +43,7 @@ const FolderPage = async ({
- + {breadcrumb.map((folder, index) => (
@@ -72,7 +72,6 @@ const FolderPage = async ({ key={folder.id} name={folder.name} color={folder.color} - spaceId={params.spaceId} id={folder.id} parentId={folder.parentId} videoCount={folder.videoCount} From 031e0eadf371797bd7033860e0bd21d3c53bd1d3 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:18:08 +0300 Subject: [PATCH 10/12] cleanup --- .../dashboard/folder/[id]/components/BreadcrumbItem.tsx | 4 ++-- apps/web/app/(org)/dashboard/folder/[id]/page.tsx | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx b/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx index 05ede8208c..8e41ade757 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/BreadcrumbItem.tsx @@ -15,7 +15,7 @@ interface BreadcrumbItemProps { id: Folder.FolderId; name: string; color: "normal" | "blue" | "red" | "yellow"; - spaceId: Space.SpaceIdOrOrganisationId; + spaceId?: Space.SpaceIdOrOrganisationId | null; isLast: boolean; } @@ -64,7 +64,7 @@ export function BreadcrumbItem({ await moveVideoToFolder({ videoId: capData.id, folderId: id, - spaceId, + spaceId: spaceId ?? null, }); router.refresh(); toast.success(`"${capData.name}" moved to "${name}" folder`); diff --git a/apps/web/app/(org)/dashboard/folder/[id]/page.tsx b/apps/web/app/(org)/dashboard/folder/[id]/page.tsx index db74907d70..43c6cb999b 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/page.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/page.tsx @@ -1,6 +1,6 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { serverEnv } from "@cap/env"; -import { CurrentUser, type Folder, type Space } from "@cap/web-domain"; +import { CurrentUser, type Folder } from "@cap/web-domain"; import { Effect } from "effect"; import { notFound } from "next/navigation"; import { @@ -18,11 +18,7 @@ import { } from "./components"; import FolderVideosSection from "./components/FolderVideosSection"; -const FolderPage = async ({ - params, -}: { - params: { id: Folder.FolderId; spaceId: Space.SpaceIdOrOrganisationId }; -}) => { +const FolderPage = async ({ params }: { params: { id: Folder.FolderId } }) => { const user = await getCurrentUser(); if (!user || !user.activeOrganizationId) return notFound(); @@ -50,7 +46,6 @@ const FolderPage = async ({

/

Date: Thu, 9 Oct 2025 15:35:50 +0300 Subject: [PATCH 11/12] more cleanups --- apps/web/actions/spaces/add-videos.ts | 12 ------ apps/web/actions/spaces/remove-videos.ts | 51 ------------------------ 2 files changed, 63 deletions(-) diff --git a/apps/web/actions/spaces/add-videos.ts b/apps/web/actions/spaces/add-videos.ts index efa3f50850..55cbf8d5f9 100644 --- a/apps/web/actions/spaces/add-videos.ts +++ b/apps/web/actions/spaces/add-videos.ts @@ -37,11 +37,6 @@ export async function addVideosToSpace( } if (isAllSpacesEntry) { - console.log({ - validVideoIds, - }); - - // Check which videos already exist in sharedVideos const existingSharedVideos = await db() .select({ videoId: sharedVideos.videoId }) .from(sharedVideos) @@ -52,16 +47,11 @@ export async function addVideosToSpace( ), ); - console.log({ - existingSharedVideos, - }); - const existingVideoIds = existingSharedVideos.map((v) => v.videoId); const newVideoIds = validVideoIds.filter( (id) => !existingVideoIds.includes(id), ); - // Update existing videos to move them to root (clear folderId) if (existingVideoIds.length > 0) { await db() .update(sharedVideos) @@ -101,7 +91,6 @@ export async function addVideosToSpace( (id) => !existingVideoIds.includes(id), ); - // Update existing videos to move them to root (clear folderId) if (existingVideoIds.length > 0) { await db() .update(spaceVideos) @@ -114,7 +103,6 @@ export async function addVideosToSpace( ); } - // Insert new videos if (newVideoIds.length > 0) { const spaceVideoEntries = newVideoIds.map((videoId) => ({ id: nanoId(), diff --git a/apps/web/actions/spaces/remove-videos.ts b/apps/web/actions/spaces/remove-videos.ts index 1f4ea469c7..602fd13fcf 100644 --- a/apps/web/actions/spaces/remove-videos.ts +++ b/apps/web/actions/spaces/remove-videos.ts @@ -51,33 +51,7 @@ export async function removeVideosFromSpace( inArray(sharedVideos.videoId, validVideoIds), ), ); - - // Set folderId to null for any removed videos that are in org-level folders - const folderRows = await db() - .select({ id: folders.id }) - .from(folders) - .where( - and( - isNull(folders.spaceId), - eq(folders.organizationId, user.activeOrganizationId), - ), - ); - - const folderIds = folderRows.map((f) => f.id); - - if (folderIds.length > 0) { - await db() - .update(videos) - .set({ folderId: null }) - .where( - and( - inArray(videos.id, validVideoIds), - inArray(videos.folderId, folderIds), - ), - ); - } } else { - // Remove from specific space (spaceVideos table) await db() .delete(spaceVideos) .where( @@ -86,31 +60,6 @@ export async function removeVideosFromSpace( inArray(spaceVideos.videoId, validVideoIds), ), ); - - // Set folderId to null for any removed videos that are in folders belonging to this space - const folderRows = await db() - .select({ id: folders.id }) - .from(folders) - .where( - and( - isNull(folders.spaceId), - eq(folders.organizationId, user.activeOrganizationId), - ), - ); - - const folderIds = folderRows.map((f) => f.id); - - if (folderIds.length > 0) { - await db() - .update(videos) - .set({ folderId: null }) - .where( - and( - inArray(videos.id, validVideoIds), - inArray(videos.folderId, folderIds), - ), - ); - } } revalidatePath(`/dashboard/spaces/${spaceId}`); From 95e8bd1b63f1b58e3394057e07315f4bba903691 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:42:14 +0300 Subject: [PATCH 12/12] Update remove-videos.ts --- apps/web/actions/spaces/remove-videos.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/web/actions/spaces/remove-videos.ts b/apps/web/actions/spaces/remove-videos.ts index 602fd13fcf..fb732ad87e 100644 --- a/apps/web/actions/spaces/remove-videos.ts +++ b/apps/web/actions/spaces/remove-videos.ts @@ -2,14 +2,9 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; -import { - folders, - sharedVideos, - spaceVideos, - videos, -} from "@cap/database/schema"; +import { sharedVideos, spaceVideos, videos } from "@cap/database/schema"; import type { Space, Video } from "@cap/web-domain"; -import { and, eq, inArray, isNull } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function removeVideosFromSpace( @@ -42,7 +37,6 @@ export async function removeVideosFromSpace( const isAllSpacesEntry = user.activeOrganizationId === spaceId; if (isAllSpacesEntry) { - // Remove from organization level (sharedVideos table) await db() .delete(sharedVideos) .where(