-
Notifications
You must be signed in to change notification settings - Fork 808
Move video to folder #1128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Move video to folder #1128
Conversation
- Add validation to ensure all requested videos exist in the space before updating - Prevent silent partial updates when some videos are missing from space_videos - Throw descriptive error when videos are not found in the specified space - Improves data integrity and prevents misleading success responses
WalkthroughAdds folder management: server actions to fetch all folders and move videos; a React dialog to select a destination folder; integration into the selected-caps toolbar; and supporting library APIs for folder queries and video moves across user/space/org roots with Effect-based execution and revalidation. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant SB as SelectedCapsBar
participant D as FolderSelectionDialog
participant SA as Server Actions
participant LIB as folder.ts
U->>SB: Click "Move to folder"
SB->>D: Open dialog
D->>SA: getAllFoldersAction(root)
SA->>LIB: getAllFolders(root) [Effect with CurrentUser]
LIB-->>SA: folders tree
SA-->>D: { success, folders }
U->>D: Choose destination, Confirm
D->>SA: moveVideosToFolderAction({ videoIds, targetFolderId, spaceId? })
SA->>LIB: moveVideosToFolder(...) [Effect/Direct + CurrentUser]
LIB-->>SA: { movedCount, originalFolderIds, targetFolderId }
SA->>SA: Revalidate dashboard/folders/spaces paths
SA-->>D: { success, message } / { error }
D-->>SB: onConfirm callback
SB-->>U: Show toast / update UI
sequenceDiagram
autonumber
participant SA as getAllFoldersAction
participant Auth as getCurrentUser
participant Eff as Effect runtime
participant LIB as getAllFolders
SA->>Auth: require user + activeOrganizationId
alt authorized
SA->>Eff: provide CurrentUser
Eff->>LIB: getAllFolders(root)
LIB-->>Eff: folders
Eff-->>SA: success
else unauthorized
SA-->>SA: return { success: false, error }
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx (1)
207-263
: Remove inline JSX commentsProject guidelines forbid inline/block comments in TSX/JSX. The JSX
{/* … */}
blocks at Lines 209, 240, 248, etc. need to go—replace them with self-describing markup or helper components instead.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/actions/folders/getAllFolders.ts
(1 hunks)apps/web/actions/folders/moveVideosToFolder.ts
(1 hunks)apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
(1 hunks)apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
(1 hunks)apps/web/lib/folder.ts
(2 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
apps/web/actions/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
All Groq/OpenAI calls must be implemented in Next.js Server Actions under apps/web/actions; do not place AI calls elsewhere
Files:
apps/web/actions/folders/getAllFolders.ts
apps/web/actions/folders/moveVideosToFolder.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/**/*.{ts,tsx}
: Use TanStack Query v5 for all client-side server state and data fetching in the web app
Web mutations should call Server Actions directly and perform targeted cache updates with setQueryData/setQueriesData rather than broad invalidations
Client code should use useEffectQuery/useEffectMutation and useRpcClient from apps/web/lib/EffectRuntime.ts; do not create ManagedRuntime inside components
Files:
apps/web/actions/folders/getAllFolders.ts
apps/web/actions/folders/moveVideosToFolder.ts
apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
apps/web/lib/folder.ts
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
**/*.{ts,tsx,js,jsx,rs}
📄 CodeRabbit inference engine (CLAUDE.md)
Do not add inline, block, or docstring comments in any language; code must be self-explanatory
Files:
apps/web/actions/folders/getAllFolders.ts
apps/web/actions/folders/moveVideosToFolder.ts
apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
apps/web/lib/folder.ts
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use strict TypeScript and avoid any; leverage shared types from packages
**/*.{ts,tsx}
: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by runningpnpm format
.
Files:
apps/web/actions/folders/getAllFolders.ts
apps/web/actions/folders/moveVideosToFolder.ts
apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
apps/web/lib/folder.ts
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}
: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g.,user-menu.tsx
).
Use PascalCase for React/Solid components.
Files:
apps/web/actions/folders/getAllFolders.ts
apps/web/actions/folders/moveVideosToFolder.ts
apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
apps/web/lib/folder.ts
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
apps/web/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
On the client, always use
useEffectQuery
oruseEffectMutation
from@/lib/EffectRuntime
; never callEffectRuntime.run*
directly in components.
Files:
apps/web/actions/folders/getAllFolders.ts
apps/web/actions/folders/moveVideosToFolder.ts
apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
apps/web/lib/folder.ts
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
apps/web/app/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer Server Components for initial data in the Next.js App Router and pass initialData to client components
Files:
apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
🧬 Code graph analysis (5)
apps/web/actions/folders/getAllFolders.ts (3)
apps/web/lib/server.ts (1)
runPromise
(59-71)apps/web/lib/folder.ts (1)
getAllFolders
(309-381)packages/web-domain/src/Authentication.ts (1)
CurrentUser
(7-10)
apps/web/actions/folders/moveVideosToFolder.ts (4)
apps/web/lib/folder.ts (1)
moveVideosToFolder
(383-493)packages/web-domain/src/Policy.ts (1)
Policy
(7-10)packages/web-domain/src/Authentication.ts (1)
CurrentUser
(7-10)apps/web/lib/server.ts (1)
runPromise
(59-71)
apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx (3)
packages/web-domain/src/Video.ts (3)
Video
(14-59)VideoId
(10-10)VideoId
(11-11)apps/web/app/(org)/dashboard/_components/ConfirmationDialog.tsx (1)
ConfirmationDialog
(25-72)apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx (1)
FolderSelectionDialog
(43-287)
apps/web/lib/folder.ts (4)
packages/web-backend/src/Database.ts (1)
Database
(9-19)packages/database/schema.ts (7)
folders
(219-243)spaceVideos
(577-597)sharedVideos
(295-315)videos
(245-293)comments
(317-337)users
(47-96)videoUploads
(656-662)packages/web-domain/src/Folder.ts (3)
Folder
(19-27)FolderId
(8-8)FolderId
(9-9)packages/web-domain/src/Authentication.ts (1)
CurrentUser
(7-10)
apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx (3)
apps/web/app/(org)/dashboard/Contexts.tsx (1)
useDashboardContext
(44-44)apps/web/actions/folders/getAllFolders.ts (1)
getAllFoldersAction
(9-35)apps/web/actions/folders/moveVideosToFolder.ts (1)
moveVideosToFolderAction
(17-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
- GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
// Fetch folders using the server action | ||
const { data: foldersData, isLoading } = useQuery({ | ||
queryKey: ["folders", activeOrganization?.organization.id, activeSpace?.id], | ||
queryFn: async () => { | ||
const root = activeSpace?.id | ||
? { variant: "space" as const, spaceId: activeSpace.id } | ||
: { | ||
variant: "org" as const, | ||
organizationId: activeOrganization!.organization.id, | ||
}; | ||
|
||
const result = await getAllFoldersAction(root); | ||
|
||
if (!result.success) { | ||
throw new Error(result.error || "Failed to fetch folders"); | ||
} | ||
|
||
return result.folders; | ||
}, | ||
enabled: open && !!activeOrganization?.organization.id, | ||
}); | ||
|
||
const folders = foldersData || []; | ||
|
||
// Toggle folder expansion | ||
const toggleFolderExpansion = (folderId: string) => { | ||
setExpandedFolders((prev) => { | ||
const newSet = new Set(prev); | ||
if (newSet.has(folderId)) { | ||
newSet.delete(folderId); | ||
} else { | ||
newSet.add(folderId); | ||
} | ||
return newSet; | ||
}); | ||
}; | ||
|
||
// Render folder with improved design | ||
const renderFolder = (folder: FolderWithChildren, depth = 0) => { | ||
const hasChildren = folder.children && folder.children.length > 0; | ||
const isExpanded = expandedFolders.has(folder.id); | ||
const isSelected = selectedFolderId === folder.id; | ||
|
||
return ( | ||
<div key={folder.id} className="relative"> | ||
<div | ||
onClick={() => setSelectedFolderId(folder.id)} | ||
className={` | ||
group flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer | ||
transition-colors border-l-4 | ||
${ | ||
isSelected | ||
? "bg-blue-3 border-blue-9" | ||
: "border-transparent hover:bg-gray-3" | ||
} | ||
`} | ||
style={{ marginLeft: `${depth * 16}px` }} | ||
> | ||
{/* Expand/Collapse button */} | ||
{hasChildren ? ( | ||
<button | ||
onClick={(e) => { | ||
e.stopPropagation(); | ||
toggleFolderExpansion(folder.id); | ||
}} | ||
className="flex-shrink-0 w-5 h-5 flex items-center justify-center rounded hover:bg-gray-4 transition" | ||
> | ||
<FontAwesomeIcon | ||
className="w-3.5 h-3.5 text-gray-10" | ||
icon={isExpanded ? faChevronDown : faChevronRight} | ||
/> | ||
</button> | ||
) : ( | ||
<div className="w-5 h-5 flex-shrink-0" /> | ||
)} | ||
|
||
{/* Folder icon */} | ||
<div className="flex items-center gap-2 flex-1"> | ||
<div className="flex-shrink-0 w-7 h-7 rounded-md bg-gray-2 border border-gray-6 shadow-sm flex items-center justify-center"> | ||
<FontAwesomeIcon | ||
className="w-4 h-4 text-gray-11" | ||
icon={faFolderOpen} | ||
/> | ||
</div> | ||
|
||
<div className="flex-1"> | ||
<p className="text-sm font-medium text-gray-12">{folder.name}</p> | ||
<p className="text-xs text-gray-9"> | ||
{folder.videoCount} video{folder.videoCount !== 1 ? "s" : ""} | ||
</p> | ||
</div> | ||
</div> | ||
|
||
{/* Selection indicator */} | ||
{isSelected && ( | ||
<div className="ml-auto w-5 h-5 rounded-full bg-blue-9 flex items-center justify-center"> | ||
<div className="w-2.5 h-2.5 bg-white rounded-full" /> | ||
</div> | ||
)} | ||
</div> | ||
|
||
{/* Render children if expanded */} | ||
{hasChildren && isExpanded && ( | ||
<div className="mt-1"> | ||
{folder.children.map((child) => renderFolder(child, depth + 1))} | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
const handleConfirm = async () => { | ||
try { | ||
const result = await moveVideosToFolderAction({ | ||
videoIds, | ||
targetFolderId: selectedFolderId, | ||
spaceId: activeSpace?.id, | ||
}); | ||
|
||
if (result.success) { | ||
toast.success(result.message); | ||
onConfirm(selectedFolderId); | ||
setSelectedFolderId(null); | ||
} else { | ||
toast.error(result.error); | ||
} | ||
} catch (error) { | ||
toast.error("Failed to move videos"); | ||
console.error("Error moving videos:", error); | ||
} | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Use EffectRuntime helpers for data fetching and mutations
Per the web coding guidelines, client components under apps/web
should rely on useEffectQuery
/ useEffectMutation
together with useRpcClient
rather than calling server actions through useQuery
and manual async handlers. Please refactor the folder fetch to useEffectQuery
and wrap moveVideosToFolderAction
in a useEffectMutation
, wiring the returned mutation state into your UI (spinner/disabled, success/error handling) and updating any relevant caches with setQueryData
instead of ad-hoc toasts. This keeps all client/server coordination on the standardized Effect runtime path.
and( | ||
eq(folders.parentId, folderId), | ||
eq(folders.organizationId, user.activeOrganizationId), | ||
root.variant === "space" | ||
? eq(folders.spaceId, root.spaceId) | ||
: undefined | ||
) | ||
) | ||
); | ||
|
||
return childFolders; | ||
}); | ||
|
||
export const getAllFolders = Effect.fn(function* ( | ||
root: | ||
| { variant: "user" } | ||
| { variant: "space"; spaceId: string } | ||
| { variant: "org"; organizationId: string } | ||
) { | ||
const db = yield* Database; | ||
const user = yield* CurrentUser; | ||
|
||
if (!user.activeOrganizationId) throw new Error("No active organization"); | ||
|
||
// Get all folders in one query | ||
const allFolders = yield* db.execute((db) => | ||
db | ||
.select({ | ||
id: folders.id, | ||
name: folders.name, | ||
color: folders.color, | ||
parentId: folders.parentId, | ||
organizationId: folders.organizationId, | ||
videoCount: sql<number>`( | ||
SELECT COUNT(*) FROM videos WHERE videos.folderId = folders.id | ||
)`, | ||
}) | ||
.from(folders) | ||
.where( | ||
and( | ||
eq(folders.organizationId, user.activeOrganizationId), | ||
root.variant === "space" | ||
? eq(folders.spaceId, root.spaceId) | ||
: undefined | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not surface space-scoped folders outside their space context
When root.variant
is "org"
or "user"
the query still returns folders whose spaceId
is set, so the org-level picker shows space-only folders. Selecting one will later force videos into an incompatible space context. Please add an explicit isNull(folders.spaceId)
filter for non-space roots (and the analogous guard in getChildFolders
) so that only folders that actually belong to the requested scope are returned.
-import { and, desc, eq, inArray } from "drizzle-orm";
+import { and, desc, eq, inArray, isNull } from "drizzle-orm";
...
.where(
and(
eq(folders.organizationId, user.activeOrganizationId),
- root.variant === "space"
- ? eq(folders.spaceId, root.spaceId)
- : undefined
+ root.variant === "space"
+ ? eq(folders.spaceId, root.spaceId)
+ : isNull(folders.spaceId)
)
)
Apply the same isNull(folders.spaceId)
constraint inside getChildFolders
when root.variant !== "space"
so the hierarchy stays consistent across both helpers.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
and( | |
eq(folders.parentId, folderId), | |
eq(folders.organizationId, user.activeOrganizationId), | |
root.variant === "space" | |
? eq(folders.spaceId, root.spaceId) | |
: undefined | |
) | |
) | |
); | |
return childFolders; | |
}); | |
export const getAllFolders = Effect.fn(function* ( | |
root: | |
| { variant: "user" } | |
| { variant: "space"; spaceId: string } | |
| { variant: "org"; organizationId: string } | |
) { | |
const db = yield* Database; | |
const user = yield* CurrentUser; | |
if (!user.activeOrganizationId) throw new Error("No active organization"); | |
// Get all folders in one query | |
const allFolders = yield* db.execute((db) => | |
db | |
.select({ | |
id: folders.id, | |
name: folders.name, | |
color: folders.color, | |
parentId: folders.parentId, | |
organizationId: folders.organizationId, | |
videoCount: sql<number>`( | |
SELECT COUNT(*) FROM videos WHERE videos.folderId = folders.id | |
)`, | |
}) | |
.from(folders) | |
.where( | |
and( | |
eq(folders.organizationId, user.activeOrganizationId), | |
root.variant === "space" | |
? eq(folders.spaceId, root.spaceId) | |
: undefined | |
) | |
) | |
// at the top of apps/web/lib/folder.ts | |
import { and, desc, eq, inArray, isNull } from "drizzle-orm"; | |
// ... inside getChildFolders (apply the same change here) ... | |
export const getAllFolders = Effect.fn(function* ( | |
root: | |
| { variant: "user" } | |
| { variant: "space"; spaceId: string } | |
| { variant: "org"; organizationId: string } | |
) { | |
const db = yield* Database; | |
const user = yield* CurrentUser; | |
if (!user.activeOrganizationId) throw new Error("No active organization"); | |
// Get all folders in one query | |
const allFolders = yield* db.execute((db) => | |
db | |
.select({ | |
id: folders.id, | |
name: folders.name, | |
color: folders.color, | |
parentId: folders.parentId, | |
organizationId: folders.organizationId, | |
videoCount: sql<number>`( | |
SELECT COUNT(*) FROM videos WHERE videos.folderId = folders.id | |
)`, | |
}) | |
.from(folders) | |
- .where( | |
- and( | |
- eq(folders.organizationId, user.activeOrganizationId), | |
- root.variant === "space" | |
- ? eq(folders.spaceId, root.spaceId) | |
- : undefined | |
- ) | |
.where( | |
and( | |
eq(folders.organizationId, user.activeOrganizationId), | |
root.variant === "space" | |
? eq(folders.spaceId, root.spaceId) | |
: isNull(folders.spaceId) | |
) | |
) | |
); | |
return allFolders; | |
}); |
🤖 Prompt for AI Agents
In apps/web/lib/folder.ts around lines 296 to 341, org/user-root queries
currently still return space-scoped folders because there is no explicit check
for folders.spaceId being null; update the where clauses in getAllFolders
(shown) and add the analogous isNull(folders.spaceId) condition inside
getChildFolders when root.variant !== "space" so that non-space roots only
return folders with no spaceId. Concretely, when root.variant === "space" keep
the eq(folders.spaceId, root.spaceId) check, otherwise add
isNull(folders.spaceId) to the and(...) filter; apply the same conditional logic
in getChildFolders so child-folder queries also exclude space-scoped folders for
org/user roots.
if (targetFolderId) { | ||
const targetFolder = yield* getFolderById(targetFolderId); | ||
|
||
if (targetFolder.organizationId !== user.activeOrganizationId) { | ||
throw new Error("Target folder not found or you don't have access to it"); | ||
} | ||
|
||
// Validate space context if provided | ||
if (root?.variant === "space" && targetFolder.spaceId !== root.spaceId) { | ||
throw new Error("Target folder does not belong to the specified space"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Block moves into space folders when we are not operating in that space
Even after filtering, a crafted request can still target a folder whose spaceId
is set while root
is "org"
/"user"
. That updates videos.folderId
with a space-only folder and breaks invariants. Defensive guard:
if (targetFolderId) {
const targetFolder = yield* getFolderById(targetFolderId);
if (targetFolder.organizationId !== user.activeOrganizationId) {
throw new Error("Target folder not found or you don't have access to it");
}
// Validate space context if provided
if (root?.variant === "space" && targetFolder.spaceId !== root.spaceId) {
throw new Error("Target folder does not belong to the specified space");
}
+
+ if (root?.variant !== "space" && targetFolder.spaceId !== null) {
+ throw new Error("Target folder is scoped to a space and cannot be used here");
+ }
}
This keeps org-level moves from leaking into space-only folders.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if (targetFolderId) { | |
const targetFolder = yield* getFolderById(targetFolderId); | |
if (targetFolder.organizationId !== user.activeOrganizationId) { | |
throw new Error("Target folder not found or you don't have access to it"); | |
} | |
// Validate space context if provided | |
if (root?.variant === "space" && targetFolder.spaceId !== root.spaceId) { | |
throw new Error("Target folder does not belong to the specified space"); | |
} | |
} | |
if (targetFolderId) { | |
const targetFolder = yield* getFolderById(targetFolderId); | |
if (targetFolder.organizationId !== user.activeOrganizationId) { | |
throw new Error("Target folder not found or you don't have access to it"); | |
} | |
// Validate space context if provided | |
if (root?.variant === "space" && targetFolder.spaceId !== root.spaceId) { | |
throw new Error("Target folder does not belong to the specified space"); | |
} | |
if (root?.variant !== "space" && targetFolder.spaceId !== null) { | |
throw new Error("Target folder is scoped to a space and cannot be used here"); | |
} | |
} |
🤖 Prompt for AI Agents
In apps/web/lib/folder.ts around lines 416 to 427, the code currently only
checks that targetFolder.spaceId matches root.spaceId when root.variant ===
"space", but it doesn't prevent moves into space-only folders when root is "org"
or "user"; add a defensive guard so that if root?.variant !== "space" and
targetFolder.spaceId is set (non-null/undefined), throw an error (e.g., "Target
folder does not belong to the specified space" or "Target folder not accessible
in current context"). Keep the existing equality check for the space case, and
ensure the thrown error message and access check occur before allowing updates
to videos.folderId.
r.mp4 |
This pull request introduces a new feature for moving videos ("caps") to folders from the dashboard, including both backend server actions and a new client-side dialog component. It also refactors and improves folder-related queries and UI integration. The most significant changes are grouped below:
New Folder Move Feature:
FolderSelectionDialog
React component for selecting a target folder when moving videos, with hierarchical folder display, loading states, and integration with toast notifications. (apps/web/app/(org)/dashboard/_components/FolderSelectionDialog.tsx
)SelectedCapsBar
to include a "Move to Folder" button and dialog, allowing users to move selected videos to a folder directly from the dashboard. (apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
) [1] [2] [3] [4]Server Actions and Backend Logic:
getAllFoldersAction
andmoveVideosToFolderAction
server actions to fetch folders and move videos, with permission checks, error handling, and path revalidation for cache consistency. (apps/web/actions/folders/getAllFolders.ts
,apps/web/actions/folders/moveVideosToFolder.ts
) [1] [2]Folder Query and Utility Improvements:
inArray
, formatting, and error handling). (apps/web/lib/folder.ts
) [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]These changes together enable users to organize their videos into folders more efficiently, with a modern UI and robust backend support.
Summary by CodeRabbit