Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/web/actions/folders/getAllFolders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use server";

import { getCurrentUser } from "@cap/database/auth/session";
import { CurrentUser } from "@cap/web-domain";
import { Effect } from "effect";
import { getAllFolders } from "../../lib/folder";
import { runPromise } from "../../lib/server";

export async function getAllFoldersAction(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For root.variant === "space", it feels worth enforcing membership server-side (similar to moveVideosToFolderAction with SpacesPolicy.isMember(...)). As-is, any authenticated user could pass an arbitrary spaceId and enumerate folder names/counts if they’re in the same org.

Also, organizationId in the org variant doesn’t get used (everything is scoped to user.activeOrganizationId), so either validate it matches or drop it to avoid a misleading API.

root:
| { variant: "user" }
| { variant: "space"; spaceId: string }
| { variant: "org"; organizationId: string }
) {
try {
const user = await getCurrentUser();
if (!user || !user.activeOrganizationId) {
return {
success: false as const,
error: "Unauthorized or no active organization",
};
}

const folders = await runPromise(
getAllFolders(root).pipe(Effect.provideService(CurrentUser, user))
);
return { success: true as const, folders };
} catch (error) {
console.error("Error fetching folders:", error);
return {
success: false as const,
error: "Failed to fetch folders",
};
}
}
98 changes: 98 additions & 0 deletions apps/web/actions/folders/move-videos-to-folder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use server";

import { getCurrentUser } from "@cap/database/auth/session";
import { CurrentUser, Video, Folder, Policy } from "@cap/web-domain";
import { SpacesPolicy } from "@cap/web-backend";
import { Effect } from "effect";
import { moveVideosToFolder } from "../../lib/folder";
import { runPromise } from "../../lib/server";
import { revalidatePath } from "next/cache";

interface MoveVideosToFolderParams {
videoIds: string[];
targetFolderId: string | null;
spaceId?: string | null;
}

export async function moveVideosToFolderAction({
videoIds,
targetFolderId,
spaceId,
}: MoveVideosToFolderParams) {
try {
const user = await getCurrentUser();
if (!user || !user.activeOrganizationId) {
return {
success: false as const,
error: "Unauthorized or no active organization",
};
}

const typedVideoIds = videoIds.map((id) => Video.VideoId.make(id));
const typedTargetFolderId = targetFolderId
? Folder.FolderId.make(targetFolderId)
: null;

const root = spaceId
? { variant: "space" as const, spaceId }
: { variant: "org" as const, organizationId: user.activeOrganizationId };

const moveVideosEffect = spaceId
? Effect.gen(function* () {
const spacesPolicy = yield* SpacesPolicy;

return yield* moveVideosToFolder(
typedVideoIds,
typedTargetFolderId,
root
).pipe(Policy.withPolicy(spacesPolicy.isMember(spaceId)));
}).pipe(Effect.provideService(CurrentUser, user))
: moveVideosToFolder(typedVideoIds, typedTargetFolderId, root).pipe(
Effect.provideService(CurrentUser, user)
);

const result = await runPromise(moveVideosEffect);

revalidatePath("/dashboard/caps");

if (spaceId) {
revalidatePath(`/dashboard/spaces/${spaceId}`);
result.originalFolderIds.forEach((folderId) => {
if (folderId) {
revalidatePath(`/dashboard/spaces/${spaceId}/folder/${folderId}`);
}
});
if (result.targetFolderId) {
revalidatePath(
`/dashboard/spaces/${spaceId}/folder/${result.targetFolderId}`
);
}
} else {
result.originalFolderIds.forEach((folderId) => {
if (folderId) {
revalidatePath(`/dashboard/folder/${folderId}`);
}
});
if (result.targetFolderId) {
revalidatePath(`/dashboard/folder/${result.targetFolderId}`);
}
}

return {
success: true as const,
message: `Successfully moved ${result.movedCount} video${
result.movedCount !== 1 ? "s" : ""
} to ${result.targetFolderId ? "folder" : "root"}`,
movedCount: result.movedCount,
originalFolderIds: result.originalFolderIds,
targetFolderId: result.targetFolderId,
videoCountDeltas: result.videoCountDeltas,
};
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: returning error.message to the client can leak implementation details (e.g. validation / DB errors). Might be worth mapping known, user-safe errors explicitly and falling back to a generic message for everything else.

console.error("Error moving videos to folder:", error);
return {
success: false as const,
error: error instanceof Error ? error.message : "Failed to move videos",
};
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is currently empty (0 bytes). If it’s accidental, I’d drop it — especially since it differs from folder-selection-dialog.tsx only by casing, which can cause weirdness on case-insensitive filesystems.

Empty file.
Loading
Loading