Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
73 changes: 0 additions & 73 deletions apps/web/actions/folders/updateFolder.ts

This file was deleted.

43 changes: 23 additions & 20 deletions apps/web/app/(org)/dashboard/caps/components/Folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { moveVideoToFolder } from "@/actions/folders/moveVideoToFolder";
import { updateFolder } from "@/actions/folders/updateFolder";
import { useEffectMutation } from "@/lib/EffectRuntime";
import { withRpc } from "@/lib/Rpcs";
import { ConfirmationDialog } from "../../_components/ConfirmationDialog";
Expand Down Expand Up @@ -83,6 +82,17 @@ const FolderCard = ({
},
});

const updateFolder = useEffectMutation({
mutationFn: (data: Folder.FolderUpdate) =>
withRpc((r) => r.FolderUpdate(data)),
onSuccess: () => {
toast.success("Folder name updated successfully");
router.refresh();
},
onError: () => toast.error("Failed to update folder name"),
onSettled: () => setIsRenaming(false),
});
Comment thread
oscartbeaumont marked this conversation as resolved.

useEffect(() => {
if (isRenaming && nameRef.current) {
nameRef.current.focus();
Expand Down Expand Up @@ -176,17 +186,6 @@ const FolderCard = ({
};
}, [id, name, rive, isDragOver]);

const updateFolderNameHandler = async () => {
try {
await updateFolder({ folderId: id, name: updateName });
toast.success("Folder name updated successfully");
} catch (error) {
toast.error("Failed to update folder name");
} finally {
setIsRenaming(false);
}
};

const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
Expand Down Expand Up @@ -342,18 +341,22 @@ const FolderCard = ({
rows={1}
value={updateName}
onChange={(e) => setUpdateName(e.target.value)}
onBlur={async () => {
onBlur={() => {
setIsRenaming(false);
if (updateName.trim() !== name) {
await updateFolderNameHandler();
}
if (updateName.trim() !== name)
updateFolder.mutate({
id,
name: updateName.trim(),
});
}}
onKeyDown={async (e) => {
onKeyDown={(e) => {
if (e.key === "Enter") {
setIsRenaming(false);
if (updateName.trim() !== name) {
await updateFolderNameHandler();
}
if (updateName.trim() !== name)
updateFolder.mutate({
id,
name: updateName.trim(),
});
}
}}
Comment thread
oscartbeaumont marked this conversation as resolved.
className="w-full resize-none bg-transparent border-none focus:outline-none
Expand Down
4 changes: 0 additions & 4 deletions apps/web/app/Layout/devtoolsServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { users } from "@cap/database/schema";
import { eq } from "drizzle-orm";

export async function promoteToPro() {
"use server";

if (process.env.NODE_ENV !== "development")
throw new Error("promoteToPro can only be used in development");

Expand All @@ -24,8 +22,6 @@ export async function promoteToPro() {
}

export async function demoteFromPro() {
"use server";

if (process.env.NODE_ENV !== "development")
throw new Error("demoteFromPro can only be used in development");

Expand Down
1 change: 0 additions & 1 deletion apps/web/app/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
"use server";
import { redirect } from "next/navigation";

export default async function EmbedPage() {
Expand Down
2 changes: 0 additions & 2 deletions apps/web/app/s/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use server";

import { redirect } from "next/navigation";

export default async function SharePage() {
Expand Down
78 changes: 78 additions & 0 deletions packages/web-backend/src/Folders/FoldersRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { nanoId } from "@cap/database/helpers";
import * as Db from "@cap/database/schema";
import { Folder, type Organisation, type User } from "@cap/web-domain";
import * as Dz from "drizzle-orm";
import { Effect, Option } from "effect";
import type { Schema } from "effect/Schema";
import { Database } from "../Database.ts";

export type CreateFolderInput = Omit<
Schema.Type<typeof Folder.Folder>,
"id" | "createdAt" | "updatedAt"
> & {
organizationId: Organisation.OrganisationId;
createdById: User.UserId;
};

export class FoldersRepo extends Effect.Service<FoldersRepo>()("FoldersRepo", {
effect: Effect.gen(function* () {
const db = yield* Database;

/**
* Gets a `Folder` by its ID.
*/
const getById = (id: Folder.FolderId) =>
Effect.gen(function* () {
const [folder] = yield* db.execute((db) =>
db.select().from(Db.folders).where(Dz.eq(Db.folders.id, id)),
);

return Option.fromNullable(folder);
});

const delete_ = (id: Folder.FolderId) =>
db.execute((db) => db.delete(Db.folders).where(Dz.eq(Db.folders.id, id)));

const create = (data: CreateFolderInput) =>
Effect.gen(function* () {
const id = Folder.FolderId.make(nanoId());

yield* db.execute((db) =>
db.insert(Db.folders).values([
{
...data,
id,
parentId: Option.getOrNull(data.parentId ?? Option.none()),
spaceId: Option.getOrNull(data.spaceId ?? Option.none()),
},
]),
);

return id;
});

const update = (id: Folder.FolderId, data: Partial<CreateFolderInput>) =>
Effect.gen(function* () {
yield* db.execute((db) =>
db
.update(Db.folders)
.set({
...data,
parentId: data.parentId
? Option.getOrNull(data.parentId)
: undefined,
spaceId: data.spaceId
? Option.getOrNull(data.spaceId)
: undefined,
updatedAt: new Date(),
Comment thread
oscartbeaumont marked this conversation as resolved.
})
.where(Dz.eq(Db.folders.id, id)),
);

return yield* getById(id);
});

return { getById, delete: delete_, create, update };
}),
dependencies: [Database.Default],
}) {}
11 changes: 11 additions & 0 deletions packages/web-backend/src/Folders/FoldersRpcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const FolderRpcsLive = Folder.FolderRpcs.toLayer(
() => new InternalError({ type: "database" }),
),
),

FolderCreate: (data) =>
folders
.create(data)
Expand All @@ -26,6 +27,16 @@ export const FolderRpcsLive = Folder.FolderRpcs.toLayer(
() => new InternalError({ type: "database" }),
),
),

FolderUpdate: (data) =>
folders
.update(data.id, data)
.pipe(
Effect.catchTag(
"DatabaseError",
() => new InternalError({ type: "database" }),
),
),
};
}),
);
91 changes: 89 additions & 2 deletions packages/web-backend/src/Folders/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { nanoId } from "@cap/database/helpers";
import * as Db from "@cap/database/schema";
import { CurrentUser, Folder, Policy } from "@cap/web-domain";
import {
CurrentUser,
Folder,
Organisation,
Policy,
User,
} from "@cap/web-domain";
import * as Dz from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
import { Database, type DatabaseError } from "../Database.ts";
import { FoldersPolicy } from "./FoldersPolicy.ts";

Expand Down Expand Up @@ -105,8 +112,15 @@ export class Folders extends Effect.Service<Folders>()("Folders", {
}),
);

return new Folder.Folder(folder);
return new Folder.Folder({
...folder,
organizationId: Organisation.OrganisationId.make(
user.activeOrganizationId,
),
createdById: User.UserId.make(user.id),
});
}),

/**
* Deletes a folder and all its subfolders. Videos inside the folders will be
* relocated to the root of the collection (space or My Caps) they're in
Expand All @@ -122,6 +136,79 @@ export class Folders extends Effect.Service<Folders>()("Folders", {

yield* deleteFolder(folder);
}),

update: Effect.fn("Folders.update")(function* (
folderId: Folder.FolderId,
data: Folder.FolderUpdate,
) {
const [folder] = yield* db
Comment thread
oscartbeaumont marked this conversation as resolved.
Outdated
.execute((db) =>
db.select().from(Db.folders).where(Dz.eq(Db.folders.id, folderId)),
)
.pipe(Policy.withPolicy(policy.canEdit(folderId)));
if (!folder) return yield* new Folder.NotFoundError();

// If parentId is provided and not null, verify it exists and belongs to the same organization
if (!data.parentId) return;
const parentId = data.parentId;

// Check that we're not creating an immediate circular reference
if (parentId === folderId)
return yield* new Folder.RecursiveDefinitionError();

const [parentFolder] = yield* db
.execute((db) =>
db
.select()
.from(Db.folders)
.where(
Dz.and(
Dz.eq(Db.folders.id, parentId),
Dz.eq(Db.folders.organizationId, folder.organizationId),
),
),
)
.pipe(Policy.withPolicy(policy.canEdit(parentId)));
if (!parentFolder) return yield* new Folder.ParentNotFoundError();

// Check for circular references in the folder hierarchy
let currentParentId = parentFolder.parentId;
while (currentParentId) {
if (currentParentId === folderId)
return yield* new Folder.RecursiveDefinitionError();

const parentId = currentParentId;
const [nextParent] = yield* db.execute((db) =>
db
.select()
.from(Db.folders)
.where(
Dz.and(
Dz.eq(Db.folders.id, parentId),
// This should be implied but extra tenant isolation can't hurt
Dz.eq(Db.folders.organizationId, folder.organizationId),
),
),
);

if (!nextParent) break;
currentParentId = nextParent.parentId;
}

yield* db.execute((db) =>
db
.update(Db.folders)
.set({
...(data.name ? { name: data.name } : {}),
...(data.color ? { color: data.color } : {}),
...(data.parentId ? { parentId: data.parentId } : {}),
})
.where(Dz.eq(Db.folders.id, folderId)),
);
Comment thread
oscartbeaumont marked this conversation as resolved.

revalidatePath(`/dashboard/caps`);
revalidatePath(`/dashboard/folder/${folderId}`);
}),
};
}),
dependencies: [FoldersPolicy.Default, Database.Default],
Expand Down
Loading