Repository: mfts/papermark
Pull request #1621: replicate folder structure from Data Room to all documents
Author: login: AshishViradiya153
Created: 2025-05-02T19:21:37Z | Merged: 2025-05-13T11:20:44Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - System and configuration files are now automatically excluded during folder and file uploads, resulting in cleaner and more relevant uploads.
- **Bug Fixes**
  - Improved error handling and logging for folder creation during uploads.
- **Refactor**
  - Enhanced folder creation logic for more consistent and reliable handling across different upload contexts.
  - Folder names are now automatically adjusted to ensure uniqueness by appending numeric suffixes when duplicates exist.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/components/upload-zone.tsx b/components/upload-zone.tsx
index 01e777b69..4f9edc473 100644
--- a/components/upload-zone.tsx
+++ b/components/upload-zone.tsx
@@ -1,7 +1,11 @@
 import { useRouter } from "next/router";
 
+
+
 import { useCallback, useMemo, useRef, useState } from "react";
 
+
+
 import { useTeam } from "@/context/team-context";
 import { DocumentStorageType } from "@prisma/client";
 import { useSession } from "next-auth/react";
@@ -9,21 +13,25 @@ import { DropEvent, FileRejection, useDropzone } from "react-dropzone";
 import { toast } from "sonner";
 import { mutate } from "swr";
 
+
+
 import { useAnalytics } from "@/lib/analytics";
 import { SUPPORTED_DOCUMENT_MIME_TYPES } from "@/lib/constants";
 import { DocumentData, createDocument } from "@/lib/documents/create-document";
 import { resumableUpload } from "@/lib/files/tus-upload";
+import { createFolderInBoth, createFolderInMainDocs, isSystemFile } from "@/lib/folders/create-folder";
 import { usePlan } from "@/lib/swr/use-billing";
 import useLimits from "@/lib/swr/use-limits";
 import { CustomUser } from "@/lib/types";
 import { cn } from "@/lib/utils";
 import { getSupportedContentType } from "@/lib/utils/get-content-type";
-import {
-  getFileSizeLimit,
-  getFileSizeLimits,
-} from "@/lib/utils/get-file-size-limits";
+import { getFileSizeLimit, getFileSizeLimits } from "@/lib/utils/get-file-size-limits";
 import { getPagesCount } from "@/lib/utils/get-page-number-count";
 
+
+
+
+
 // Originally these mime values were directly used in the dropzone hook.
 // There was a solid reason to take them out of the scope, primarily to solve a browser compatibility issue to determine the file type when user dropped a folder.
 // you will figure out how this change helped to fix the compatibility issue once you have went through reading of `getFilesFromDropEvent` and `traverseFolder`
@@ -469,6 +477,10 @@ export default function UploadZone({
 
         let files: FileWithPaths[] = [];
 
+        if (isSystemFile(entry.name)) {
+          return files;
+        }
+
         if (entry.isDirectory) {
           /**
            * Let's create the folder.
@@ -499,55 +511,48 @@ export default function UploadZone({
               throw new Error("No team found");
             }
 
-            const response = await fetch(
-              `/api/teams/${teamInfo.currentTeam.id}/${endpointTargetType}`,
-              {
-                method: "POST",
-                headers: {
-                  "Content-Type": "application/json",
-                },
-                body: JSON.stringify({
-                  name: entry.name, // as folderName
-                  path: parentPathOfThisEntry ?? folderPathName,
-                }),
-              },
-            );
-
-            if (!response.ok) {
-              const { message } = await response.json();
-              setRejectedFiles((prev) => [
-                {
-                  fileName: entry.name,
-                  message: message,
-                },
-                ...prev,
-              ]);
-            } else {
-              let {
-                parentFolderPath: parentFolderPath,
-                path: slugifiedPathNameOfThisEntryAfterFolderCreation,
-              } = await response.json();
-
-              if (
-                slugifiedPathNameOfThisEntryAfterFolderCreation?.startsWith("/")
-              ) {
-                // Reason "/" is removed because our `createDocument` API needs `path` to not start with "/"
-                slugifiedPathNameOfThisEntryAfterFolderCreation =
-                  slugifiedPathNameOfThisEntryAfterFolderCreation.slice(1);
-              }
+            // Create folder in main documents if not in dataroom
+            if (!dataroomId) {
+              // Create folder in main documents only
+              const { path: folderPath } = await createFolderInMainDocs({
+                teamId: teamInfo.currentTeam.id,
+                name: entry.name,
+                path: parentPathOfThisEntry ?? folderPathName,
+              });
 
               analytics.capture("Folder Added", { folderName: entry.name });
-              mutate(
-                `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}?root=true`,
-              );
-              mutate(
-                `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}`,
+
+              const dirReader = (
+                entry as FileSystemDirectoryEntry
+              ).createReader();
+              const subEntries = await new Promise<FileSystemEntry[]>(
+                (resolve) => dirReader.readEntries(resolve),
               );
-              mutate(
-                `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}${parentFolderPath}`,
+
+              const filteredSubEntries = subEntries.filter(
+                (subEntry) => !isSystemFile(subEntry.name),
               );
+              for (const subEntry of filteredSubEntries) {
+                files.push(
+                  ...(await traverseFolder(
+                    subEntry,
+                    folderPath.startsWith("/")
+                      ? folderPath.slice(1)
+                      : folderPath,
+                  )),
+                );
+              }
+            } else {
+              // Create folder in both dataroom and main documents
+              const { dataroomPath } = await createFolderInBoth({
+                teamId: teamInfo.currentTeam.id,
+                dataroomId,
+                name: entry.name,
+                path: parentPathOfThisEntry ?? folderPathName,
+                setRejectedFiles,
+                analytics,
+              });
 
-              // Now we are sure that folder is created at the backend and we can continue to traverse its child folders/files.
               const dirReader = (
                 entry as FileSystemDirectoryEntry
               ).createReader();
@@ -555,16 +560,26 @@ export default function UploadZone({
                 (resolve) => dirReader.readEntries(resolve),
               );
 
-              for (const subEntry of subEntries) {
+              const filteredSubEntries = subEntries.filter(
+                (subEntry) => !isSystemFile(subEntry.name),
+              );
+
+              for (const subEntry of filteredSubEntries) {
                 files.push(
                   ...(await traverseFolder(
                     subEntry,
-                    slugifiedPathNameOfThisEntryAfterFolderCreation,
+                    dataroomPath.startsWith("/")
+                      ? dataroomPath.slice(1)
+                      : dataroomPath,
                   )),
                 );
               }
             }
           } catch (error) {
+            console.error(
+              "An error occurred while creating the folder: ",
+              error,
+            );
             setRejectedFiles((prev) => [
               {
                 fileName: entry.name,
@@ -574,6 +589,10 @@ export default function UploadZone({
             ]);
           }
         } else if (entry.isFile) {
+          if (isSystemFile(entry.name)) {
+            return files;
+          }
+
           let file = await new Promise<FileWithPaths>((resolve) =>
             (entry as FileSystemFileEntry).file(resolve),
           );
@@ -704,4 +723,4 @@ export default function UploadZone({
       {children}
     </div>
   );
-}
+}
\ No newline at end of file
diff --git a/lib/constants.ts b/lib/constants.ts
index 93c47776b..ee72a4369 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -412,3 +412,5 @@ export const EU_COUNTRY_CODES = [
   "SI",
   "SK",
 ];
+
+export const SYSTEM_FILES = [".DS_Store", "Thumbs.db", "node_modules"];
diff --git a/lib/folders/create-folder.ts b/lib/folders/create-folder.ts
new file mode 100644
index 000000000..f84353dc1
--- /dev/null
+++ b/lib/folders/create-folder.ts
@@ -0,0 +1,128 @@
+import { mutate } from "swr";
+
+import { SYSTEM_FILES } from "../constants";
+
+export function isSystemFile(name: string): boolean {
+  return (
+    SYSTEM_FILES.includes(name.toLowerCase()) ||
+    name.toLowerCase().startsWith(".")
+  );
+}
+
+interface CreateFolderResponse {
+  path: string;
+  parentFolderPath?: string;
+}
+
+export async function createFolderInDataroom({
+  teamId,
+  dataroomId,
+  name,
+  path,
+}: {
+  teamId: string;
+  dataroomId: string;
+  name: string;
+  path?: string;
+}): Promise<CreateFolderResponse> {
+  const response = await fetch(
+    `/api/teams/${teamId}/datarooms/${dataroomId}/folders`,
+    {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+        name,
+        path,
+      }),
+    },
+  );
+
+  if (!response.ok) {
+    const error = await response.json();
+    throw new Error(error.message || "Failed to create folder in dataroom");
+  }
+
+  return response.json();
+}
+
+export async function createFolderInMainDocs({
+  teamId,
+  name,
+  path,
+}: {
+  teamId: string;
+  name: string;
+  path?: string;
+}): Promise<CreateFolderResponse> {
+  const response = await fetch(`/api/teams/${teamId}/folders`, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify({
+      name,
+      path,
+    }),
+  });
+
+  if (!response.ok) {
+    const error = await response.json();
+    throw new Error(
+      error.message || "Failed to create folder in all documents",
+    );
+  }
+
+  return response.json();
+}
+
+export async function createFolderInBoth({
+  teamId,
+  dataroomId,
+  name,
+  path,
+  setRejectedFiles,
+  analytics,
+}: {
+  teamId: string;
+  dataroomId: string;
+  name: string;
+  path?: string;
+  setRejectedFiles: (files: { fileName: string; message: string }[]) => void;
+  analytics: any;
+}): Promise<{ dataroomPath: string; mainDocsPath: string }> {
+  try {
+    const [dataroomResponse, mainDocsResponse] = await Promise.all([
+      createFolderInDataroom({ teamId, dataroomId, name, path }),
+      createFolderInMainDocs({ teamId, name, path }),
+    ]);
+
+    // Track analytics
+    analytics.capture("Folder Added in dataroom", {
+      folderName: name,
+    });
+
+    // Mutate dataroom folders
+    mutate(`/api/teams/${teamId}/datarooms/${dataroomId}/folders?root=true`);
+    mutate(`/api/teams/${teamId}/datarooms/${dataroomId}/folders`);
+    mutate(
+      `/api/teams/${teamId}/datarooms/${dataroomId}/folders/${dataroomResponse.path}`,
+    );
+
+    return {
+      dataroomPath: dataroomResponse.path,
+      mainDocsPath: mainDocsResponse.path,
+    };
+  } catch (error) {
+    console.error("An error occurred while creating the folder: ", error);
+    setRejectedFiles([
+      {
+        fileName: name,
+        message:
+          error instanceof Error ? error.message : "Failed to create folder",
+      },
+    ]);
+    throw error;
+  }
+}
diff --git a/pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts b/pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts
index 1d406f5ff..79d29e5bc 100644
--- a/pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts
+++ b/pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts
@@ -206,9 +206,9 @@ export default async function handle(
 
     const { name, path } = req.body as { name: string; path?: string };
 
-    const childFolderPath = path
-      ? "/" + path + "/" + slugify(name)
-      : "/" + slugify(name);
+    // const childFolderPath = path
+    //   ? "/" + path + "/" + slugify(name)
+    //   : "/" + slugify(name);
 
     const parentFolderPath = path ? "/" + path : "/";
 
@@ -243,9 +243,42 @@ export default async function handle(
         },
       });
 
+      // Duplicate name handling
+      let folderName = name;
+      let counter = 1;
+      const MAX_RETRIES = 50;
+
+      let childFolderPath = path
+        ? "/" + path + "/" + slugify(folderName)
+        : "/" + slugify(folderName);
+
+      while (counter <= MAX_RETRIES) {
+        const existingFolder = await prisma.dataroomFolder.findUnique({
+          where: {
+            dataroomId_path: {
+              dataroomId: dataroomId,
+              path: childFolderPath,
+            },
+          },
+        });
+        if (!existingFolder) break;
+        folderName = `${name} (${counter})`;
+        childFolderPath = path
+          ? "/" + path + "/" + slugify(folderName)
+          : "/" + slugify(folderName);
+        counter++;
+      }
+
+      if (counter > MAX_RETRIES) {
+        return res.status(400).json({
+          error: "Failed to create folder",
+          message: "Too many folders with similar names",
+        });
+      }
+
       const folder = await prisma.dataroomFolder.create({
         data: {
-          name: name,
+          name: folderName,
           path: childFolderPath,
           parentId: parentFolder?.id ?? null,
           dataroomId: dataroomId,
@@ -265,8 +298,8 @@ export default async function handle(
       res.status(500).json({ error: "Error creating folder" });
     }
   } else {
-    // We only allow POST requests
-    res.setHeader("Allow", ["GET"]);
+    // We only allow GET and POST requests
+    res.setHeader("Allow", ["GET", "POST"]);
     return res.status(405).end(`Method ${req.method} Not Allowed`);
   }
 }
diff --git a/pages/api/teams/[teamId]/folders/index.ts b/pages/api/teams/[teamId]/folders/index.ts
index 3f1453c6c..1d820fa7a 100644
--- a/pages/api/teams/[teamId]/folders/index.ts
+++ b/pages/api/teams/[teamId]/folders/index.ts
@@ -105,9 +105,9 @@ export default async function handle(
     const { teamId } = req.query as { teamId: string };
     const { name, path } = req.body as { name: string; path: string };
 
-    const childFolderPath = path
-      ? "/" + path + "/" + slugify(name)
-      : "/" + slugify(name);
+    // const childFolderPath = path
+    //   ? "/" + path + "/" + slugify(name)
+    //   : "/" + slugify(name);
 
     const parentFolderPath = path ? "/" + path : "/";
 
@@ -142,9 +142,43 @@ export default async function handle(
         },
       });
 
+      let folderName = name;
+      let counter = 1;
+      const MAX_RETRIES = 50;
+
+      let childFolderPath = path
+        ? "/" + path + "/" + slugify(folderName)
+        : "/" + slugify(folderName);
+
+      while (counter <= MAX_RETRIES) {
+        const existingFolder = await prisma.folder.findUnique({
+          where: {
+            teamId_path: {
+              teamId: teamId,
+              path: childFolderPath,
+            },
+          },
+        });
+
+        if (!existingFolder) break;
+
+        folderName = `${name} (${counter})`;
+        childFolderPath = path
+          ? "/" + path + "/" + slugify(folderName)
+          : "/" + slugify(folderName);
+        counter++;
+      }
+
+      if (counter > MAX_RETRIES) {
+        return res.status(400).json({
+          error: "Failed to create folder",
+          message: "Too many folders with similar names",
+        });
+      }
+
       const folder = await prisma.folder.create({
         data: {
-          name: name,
+          name: folderName,
           path: childFolderPath,
           parentId: parentFolder?.id ?? null,
           teamId: teamId,


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>components/upload-zone.tsx (2)</summary><blockquote>

`444-446`: **Great early-exit filter, but make it case-insensitive**

`isSystemFile(entry.name)` will miss `Node_Modules`, `README.MD`, etc. on case-insensitive file-systems.  
Safest fix is to lowercase once:

```diff
-if (isSystemFile(entry.name)) {
+if (isSystemFile(entry.name.toLowerCase())) {
   return files;
 }
```

…and likewise inside `isSystemFile` itself (see comment in create-folder.ts).

---

`556-559`: **Redundant system-file check**

This second guard is never reached because the function already returned early for any system file at line 444.  
Remove to avoid dead code:

```diff
-  if (isSystemFile(entry.name)) {
-    return files;
-  }
```

</blockquote></details>
<details>
<summary>lib/folders/create-folder.ts (3)</summary><blockquote>

`4-6`: **`isSystemFile` should normalise case and remove duplication**

```diff
-export function isSystemFile(name: string): boolean {
-    return SYSTEM_FILES.includes(name) || name.startsWith('.');
+export function isSystemFile(name: string): boolean {
+    const normalised = name.toLowerCase();
+    return (
+        SYSTEM_FILES.has?.(normalised) // if switched to a Set
+        || SYSTEM_FILES.includes?.(normalised) // fallback for array
+        || normalised.startsWith('.')
+    );
 }
```

A Set gives O(1) lookups and sidesteps multiple `includes` scans.

---

`55-65`: **Avoid sending `"path": undefined` in the POST body**

Some back-ends reject unknown/undefined values. Create the JSON conditionally:

```diff
-body: JSON.stringify({
-  name,
-  path,
-}),
+body: JSON.stringify({
+  name,
+  ...(path ? { path } : {}),
+}),
```
Same for dataroom variant.

---

`100-104`: **Refresh main-docs SWR cache too**

`createFolderInBoth` mutates dataroom folders but not the corresponding main-documents endpoints (`/api/teams/${teamId}/folders…`).  
Add analogous `mutate` calls so both views stay consistent after the Promise resolves.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d699f0bb2355d9bcc9bab75b9506a8e3a1956f8a and 5f0fc8f2bf8409c56a5d811d9569c75675c0c444.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `components/upload-zone.tsx` (5 hunks)
* `lib/constants.ts` (1 hunks)
* `lib/folders/create-folder.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>components/upload-zone.tsx (1)</summary><blockquote>

<details>
<summary>lib/folders/create-folder.ts (3)</summary>

* `isSystemFile` (4-6)
* `createFolderInMainDocs` (46-72)
* `createFolderInBoth` (74-119)

</details>

</blockquote></details>
<details>
<summary>lib/folders/create-folder.ts (1)</summary><blockquote>

<details>
<summary>lib/constants.ts (1)</summary>

* `SYSTEM_FILES` (418-507)

</details>

</blockquote></details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T09:29:59Z
user: login: mfts
body: This is a massive one! 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-13T11:06:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts (1)</summary><blockquote>

`246-269`: **Solid implementation of duplicate folder name handling.**

The implementation ensures unique folder paths by appending numeric suffixes to folder names when collisions are detected. This approach is robust and consistent with the folder handling in the main documents section.

Two suggestions for consideration:
1. Consider adding a maximum retry limit (e.g., 100) to prevent potential infinite loops in case of database issues.
2. Consider extracting this duplicate name resolution logic into a shared utility function since it's now implemented in multiple places.

 
```diff
// Example of adding a maximum retry limit
let folderName = name;
let counter = 1;
+ const MAX_RETRIES = 100;

let childFolderPath = path
  ? "/" + path + "/" + slugify(folderName)
  : "/" + slugify(folderName);

- while (true) {
+ while (counter <= MAX_RETRIES) {
  const existingFolder = await prisma.dataroomFolder.findUnique({
    where: {
      dataroomId_path: {
        dataroomId: dataroomId,
        path: childFolderPath,
      },
    },
  });
  if (!existingFolder) break;
  folderName = `${name} (${counter})`;
  childFolderPath = path
    ? "/" + path + "/" + slugify(folderName)
    : "/" + slugify(folderName);
  counter++;
}
+ // Handle the case where we've exceeded the maximum retries
+ if (counter > MAX_RETRIES) {
+   throw new Error("Could not create folder: too many folders with similar names");
+ }
```

</blockquote></details>
<details>
<summary>pages/api/teams/[teamId]/folders/index.ts (1)</summary><blockquote>

`145-169`: **Solid implementation of duplicate folder name handling.**

The implementation ensures unique folder paths by appending numeric suffixes to folder names when collisions are detected. The approach is consistent with the implementation in the datarooms API.

One improvement to consider:
1. As suggested for the datarooms endpoint, consider adding a maximum retry limit to prevent potential infinite loops.
2. Consider extracting this duplicate name handling logic into a shared utility function since it's implemented in both this file and the datarooms API.

 
```diff
let folderName = name;
let counter = 1;
+ const MAX_RETRIES = 100;

let childFolderPath = path
  ? "/" + path + "/" + slugify(folderName)
  : "/" + slugify(folderName);

- while (true) {
+ while (counter <= MAX_RETRIES) {
  const existingFolder = await prisma.folder.findUnique({
    where: {
      teamId_path: {
        teamId: teamId,
        path: childFolderPath,
      },
    },
  });

  if (!existingFolder) break;

  folderName = `${name} (${counter})`;
  childFolderPath = path
    ? "/" + path + "/" + slugify(folderName)
    : "/" + slugify(folderName);
  counter++;
}
+ // Handle the case where we've exceeded the maximum retries
+ if (counter > MAX_RETRIES) {
+   throw new Error("Could not create folder: too many folders with similar names");
+ }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e7a2707720508f07e0c8a5df7b25996f28ed616f and 3128b21629ee7548bd978bda4f77b965097a327e.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `lib/constants.ts` (1 hunks)
* `lib/folders/create-folder.ts` (1 hunks)
* `pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts` (3 hunks)
* `pages/api/teams/[teamId]/folders/index.ts` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* lib/constants.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* lib/folders/create-folder.ts

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts (3)</summary>

`209-211`: **Good removal of deprecated logic.**

The old approach for generating folder paths has been properly commented out before implementing the new solution.

---

`273-274`: **Correctly implemented folder creation with unique name.**

The folder creation now uses the potentially modified folderName, which ensures unique path creation.

---

`294-294`: **Updated HTTP method header correctly.**

The Allow header now correctly lists both GET and POST methods supported by this endpoint.

</details>
<details>
<summary>pages/api/teams/[teamId]/folders/index.ts (2)</summary>

`108-110`: **Good removal of deprecated logic.**

The old approach for generating folder paths has been properly commented out before implementing the new solution.

---

`173-173`: **Correctly implemented folder creation with unique name.**

The folder creation now uses the potentially modified folderName, which ensures unique path creation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T11:07:02Z

Review comments:
diff_hunk: @@ -412,3 +412,96 @@ export const EU_COUNTRY_CODES = [
   "SI",
   "SK",
 ];
+
+
+
+export const SYSTEM_FILES = [
+  '.DS_Store',
+  'Thumbs.db',
+  '.git',
+  '.gitignore',
+  '.svn',
+  '.idea',
+  '.vscode',
+  'node_modules',
+  'package-lock.json',
+  'yarn.lock',
+  'package.json',
+  'tsconfig.json',
+  'README.md',
+  'LICENSE',
+  '.env',
+  '.env.local',
+  '.env.development',
+  '.env.production',
+  '.env.test',
+  '.npmrc',
+  '.yarnrc',
+  '.editorconfig',
+  '.prettierrc',
+  '.eslintrc',
+  '.babelrc',
+  '.travis.yml',
+  'docker-compose.yml',
+  'Dockerfile',
+  'dockerfile',
+  '.dockerignore',
+  '.gitattributes',
+  '.gitmodules',
+  '.gitkeep',
+  '.gitlab-ci.yml',
+  '.circleci',
+  '.github',
+  '.husky',
+  '.lintstagedrc',
+  '.npmignore',
+  '.yarn',
+  '.pnp.js',
+  '.pnp.cjs',
+  '.pnp.mjs',
+  '.pnp.loader.js',
+  '.pnp.loader.cjs',
+  '.pnp.loader.mjs',
+  '.pnp.loader.json',
+  '.pnp.loader.yaml',
+  '.pnp.loader.yml',
+  '.pnp.loader.toml',
+  '.pnp.loader.ini',
+  '.pnp.loader.jsx',
+  '.pnp.loader.ts',
+  '.pnp.loader.tsx',
+  '.pnp.loader.vue',
+  '.pnp.loader.svelte',
+  '.pnp.loader.astro',
+  '.pnp.loader.mdx',
+  '.pnp.loader.md',
+  '.pnp.loader.html',
+  '.pnp.loader.css',
+  '.pnp.loader.scss',
+  '.pnp.loader.sass',
+  '.pnp.loader.less',
+  '.pnp.loader.styl',
+  '.pnp.loader.stylus',
+  '.pnp.loader.postcss',
+  '.pnp.loader.json',
+  '.pnp.loader.yaml',
+  '.pnp.loader.yml',
+  '.pnp.loader.toml',
+  '.pnp.loader.ini',
+  '.pnp.loader.jsx',
+  '.pnp.loader.ts',
+  '.pnp.loader.tsx',
+  '.pnp.loader.vue',
+  '.pnp.loader.svelte',
+  '.pnp.loader.astro',
+  '.pnp.loader.mdx',
+  '.pnp.loader.md',
+  '.pnp.loader.html',
+  '.pnp.loader.css',
+  '.pnp.loader.scss',
+  '.pnp.loader.sass',
+  '.pnp.loader.less',
+  '.pnp.loader.styl',
+  '.pnp.loader.stylus',
+  '.pnp.loader.postcss',
+];
path: lib/constants.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Duplicate & exhaustive list inflates bundle size and is hard to maintain**

SYSTEM_FILES contains many repeated `.pnp.loader.*` entries and an exhaustive enumeration of variations that could be expressed more concisely (e.g. `.pnp.loader.*`).  
The duplication:

* increases bundle/transmit size for every client
* makes manual maintenance error-prone (future omissions / typos)
* forces an O(N) lookup every time `isSystemFile` is called (negligible today, but still unnecessary)

Consider:

```diff
-export const SYSTEM_FILES = [
-  '.pnp.loader.js',
-  '.pnp.loader.cjs',
-  ...
-  '.pnp.loader.postcss',
-];
+// NB: All dot-files are already filtered by the leading-dot check in `isSystemFile`
+const GLOB_BASED_PATTERNS = ['.pnp.loader.*'];
+
+export const SYSTEM_FILES: ReadonlySet<string> = new Set([
+  '.DS_Store',
+  'Thumbs.db',
+  'node_modules',
+  // …
+]);
+
+export function matchesSystemPattern(name: string): boolean {
+  return GLOB_BASED_PATTERNS.some((p) =>
+    new RegExp(
+      '^' + p.replace('.', '\\.').replace('*', '.*') + '$',
+      'i',
+    ).test(name),
+  );
+}
```

…and update `isSystemFile` to consult the Set + pattern helper.  
This removes duplicates, gives O(1) membership tests, and drastically reduces the constant footprint.  



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T09:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -499,72 +475,75 @@ export default function UploadZone({
               throw new Error("No team found");
             }
 
-            const response = await fetch(
-              `/api/teams/${teamInfo.currentTeam.id}/${endpointTargetType}`,
-              {
-                method: "POST",
-                headers: {
-                  "Content-Type": "application/json",
-                },
-                body: JSON.stringify({
-                  name: entry.name, // as folderName
-                  path: parentPathOfThisEntry ?? folderPathName,
-                }),
-              },
-            );
-
-            if (!response.ok) {
-              const { message } = await response.json();
-              setRejectedFiles((prev) => [
-                {
-                  fileName: entry.name,
-                  message: message,
-                },
-                ...prev,
-              ]);
-            } else {
-              let {
-                parentFolderPath: parentFolderPath,
-                path: slugifiedPathNameOfThisEntryAfterFolderCreation,
-              } = await response.json();
-
-              if (
-                slugifiedPathNameOfThisEntryAfterFolderCreation?.startsWith("/")
-              ) {
-                // Reason "/" is removed because our `createDocument` API needs `path` to not start with "/"
-                slugifiedPathNameOfThisEntryAfterFolderCreation =
-                  slugifiedPathNameOfThisEntryAfterFolderCreation.slice(1);
-              }
+            // Create folder in main documents if not in dataroom
+            if (!dataroomId) {
+              // Create folder in main documents only
+              const { path: folderPath } = await createFolderInMainDocs({
+                teamId: teamInfo.currentTeam.id,
+                name: entry.name,
+                path: parentPathOfThisEntry ?? folderPathName,
+              });
 
               analytics.capture("Folder Added", { folderName: entry.name });
-              mutate(
-                `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}?root=true`,
-              );
-              mutate(
-                `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}`,
+
+              const dirReader = (
+                entry as FileSystemDirectoryEntry
+              ).createReader();
+              const subEntries = await new Promise<FileSystemEntry[]>(
+                (resolve) => dirReader.readEntries(resolve),
               );
-              mutate(
-                `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}${parentFolderPath}`,
+
+              const filteredSubEntries = subEntries.filter(
+                (subEntry) => !isSystemFile(subEntry.name),
               );
+              for (const subEntry of filteredSubEntries) {
+                files.push(
+                  ...(await traverseFolder(
+                    subEntry,
+                    folderPath.startsWith("/")
+                      ? folderPath.slice(1)
+                      : folderPath,
+                  )),
+                );
+              }
+            } else {
path: components/upload-zone.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Potential duplicate folder creation → race conditions**

`traverseFolder` calls `createFolderInMainDocs` for *every* encounter of the same directory name in the walk (e.g. parent and multiple sibling files).  
Back-end might 409/500 on the 2ⁿᵈ attempt and you’ll still recurse into children.

Consider memoising created paths:

```diff
+const createdFolders = new Set<string>();

// …

-const { path: folderPath } = await createFolderInMainDocs({...});
+if (!createdFolders.has(parentPathOfThisEntry ?? '')) {
+  const { path: folderPath } = await createFolderInMainDocs({...});
+  createdFolders.add(folderPath);
+}
```

Same applies to the dataroom branch.  
This avoids unnecessary network traffic and log noise.  



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T09:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+import { mutate } from "swr";
+import { SYSTEM_FILES } from "../constants";
+
+export function isSystemFile(name: string): boolean {
+    return SYSTEM_FILES.includes(name) || name.startsWith('.');
+}
+
+interface CreateFolderResponse {
+    path: string;
+    parentFolderPath?: string;
+}
+
+export async function createFolderInDataroom({
+    teamId,
+    dataroomId,
+    name,
+    path,
+}: {
+    teamId: string;
+    dataroomId: string;
+    name: string;
+    path?: string;
+}): Promise<CreateFolderResponse> {
+    const response = await fetch(
+        `/api/teams/${teamId}/datarooms/${dataroomId}/folders`,
+        {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/json",
+            },
+            body: JSON.stringify({
+                name,
+                path,
+            }),
+        },
+    );
+
+    if (!response.ok) {
+        const error = await response.json();
+        throw new Error(error.message || "Failed to create folder in dataroom");
+    }
+
+    return response.json();
+}
path: lib/folders/create-folder.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Error-parsing can throw; fall back to `text()`**

If the server responds with a non-JSON body (e.g. 502 proxy HTML) the attempted `response.json()` will itself throw, masking the original error.

```diff
-const error = await response.json();
-throw new Error(error.message || "Failed to create folder in dataroom");
+let message = "Failed to create folder in dataroom";
+try {
+  const errJson = await response.clone().json();
+  message = errJson.message ?? message;
+} catch {
+  message = await response.text().catch(() => message);
+}
+throw new Error(message);
```
Repeat for `createFolderInMainDocs`.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    const response = await fetch(
        `/api/teams/${teamId}/datarooms/${dataroomId}/folders`,
        {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                name,
                path,
            }),
        },
    );

    if (!response.ok) {
        let message = "Failed to create folder in dataroom";
        try {
            const errJson = await response.clone().json();
            message = errJson.message ?? message;
        } catch {
            message = await response.text().catch(() => message);
        }
        throw new Error(message);
    }

    return response.json();
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T09:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,9 +142,35 @@ export default async function handle(
         },
       });
 
+      let folderName = name;
+      let counter = 1;
+
+      let childFolderPath = path
+        ? "/" + path + "/" + slugify(folderName)
+        : "/" + slugify(folderName);
+
+      while (true) {
+        const existingFolder = await prisma.folder.findUnique({
+          where: {
+            teamId_path: {
+              teamId: teamId,
+              path: childFolderPath,
+            },
+          },
+        });
+
+        if (!existingFolder) break;
+
+        folderName = `${name} (${counter})`;
+        childFolderPath = path
+          ? "/" + path + "/" + slugify(folderName)
+          : "/" + slugify(folderName);
+        counter++;
+      }
+
       const folder = await prisma.folder.create({
         data: {
-          name: name,
+          name: folderName,
           path: childFolderPath,
path: pages/api/teams/[teamId]/folders/index.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider creating a shared utility for duplicate name handling.**

Since this exact pattern for handling duplicate folder names appears in both this file and the datarooms folder API, consider extracting it to a shared utility function. This would improve maintainability and reduce code duplication.


```typescript
// Example shared utility (could be placed in a new file like lib/utils/folder-paths.ts)
export async function generateUniqueFolderPath({
  name,
  path,
  existsInDB,
}: {
  name: string;
  path?: string;
  existsInDB: (path: string) => Promise<boolean>;
}) {
  let folderName = name;
  let counter = 1;
  const MAX_RETRIES = 100;

  let folderPath = path
    ? "/" + path + "/" + slugify(folderName)
    : "/" + slugify(folderName);

  while (counter <= MAX_RETRIES) {
    const exists = await existsInDB(folderPath);
    if (!exists) break;
    
    folderName = `${name} (${counter})`;
    folderPath = path
      ? "/" + path + "/" + slugify(folderName)
      : "/" + slugify(folderName);
    counter++;
  }
  
  if (counter > MAX_RETRIES) {
    throw new Error("Could not create folder: too many folders with similar names");
  }
  
  return { folderName, folderPath };
}

// Usage in this file would be:
const { folderName, folderPath: childFolderPath } = await generateUniqueFolderPath({
  name,
  path,
  existsInDB: async (path) => {
    const existingFolder = await prisma.folder.findUnique({
      where: {
        teamId_path: {
          teamId: teamId,
          path,
        },
      },
    });
    return !!existingFolder;
  },
});
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T11:07:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-02T19:21:41Z
body: @AshishViradiya153 is attempting to deploy a commit to the **mftsio** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=mftsio&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%225f0fc8f2bf8409c56a5d811d9569c75675c0c444%22%7D%2C%22id%22%3A%22QmZvXViWTnp5k314XVTe9mkpgfHASHykAh654XMsve2ZQR%22%2C%22org%22%3A%22mfts%22%2C%22prId%22%3A1621%2C%22repo%22%3A%22papermark%22%7D).


author_association: NONE
user: login: mfts
created_at: 2025-05-13T09:26:12Z
body: @coderabbitai full review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-13T09:26:17Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Full review triggered.

</details>
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-13T09:26:20Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

The changes introduce system file filtering in the upload zone by adding checks to exclude files and folders matching a new `SYSTEM_FILES` constant. Folder creation logic is refactored to use new helper functions from a dedicated module, which also provides an `isSystemFile` utility. Error handling, analytics integration, and cache invalidation are enhanced for folder creation. Additionally, API endpoints for folder creation now ensure unique folder paths by appending numeric suffixes to duplicate names.

## Changes

| File(s)                                             | Change Summary                                                                                                   |
|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| components/upload-zone.tsx                          | Added system file filtering using `isSystemFile`; refactored folder creation to use helper functions; improved error handling and analytics integration during folder and file processing. |
| lib/constants.ts                                    | Introduced `SYSTEM_FILES` constant array listing common system/configuration files and directories.              |
| lib/folders/create-folder.ts                        | New module with exported functions for folder creation in dataroom/main docs, `isSystemFile` utility, and error handling with analytics and cache refresh logic. |
| pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts | Modified folder creation to ensure unique folder paths by appending numeric suffixes to duplicate folder names; updated allowed HTTP methods header. |
| pages/api/teams/[teamId]/folders/index.ts           | Implemented uniqueness check for folder paths by appending numeric suffixes to duplicate folder names before creation. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant UploadZone
    participant CreateFolderModule
    participant Analytics
    participant SWRCache

    User->>UploadZone: Uploads folder/files
    UploadZone->>CreateFolderModule: Check isSystemFile for each entry
    alt Not a system file
        UploadZone->>CreateFolderModule: createFolderInBoth (if dataroom)
        CreateFolderModule->>Analytics: Track folder addition
        CreateFolderModule->>SWRCache: Refresh folder data
        CreateFolderModule-->>UploadZone: Return created folder paths
        UploadZone->>UploadZone: Process child entries (recursively, filtering system files)
    else Is a system file
        UploadZone->>UploadZone: Skip processing
    end
    alt Error occurs
        CreateFolderModule-->>UploadZone: Log error, set rejected files
    end
```

## Poem

> 🐇  
> In the warren of files, I hop and I sift,  
> Skipping system clutter, what a nifty gift!  
> Folders now blossom, both here and there,  
> With helpers and checks, handled with care.  
> No more .DS_Store in my carrot patch—  
> Just clean, happy docs—what a marvelous catch!  
>
"""

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3128b21629ee7548bd978bda4f77b965097a327e and a8bfecf52ab6332aae676de980754a97f74a83d0.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts` (3 hunks)
* `pages/api/teams/[teamId]/folders/index.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* pages/api/teams/[teamId]/datarooms/[id]/folders/index.ts
* pages/api/teams/[teamId]/folders/index.ts

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=mfts/papermark&utm_content=1621):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: vercel[bot]
created_at: 2025-05-13T11:06:18Z
body: [vc]: #cCPpE/OwZWsWaEdCBgwuvO+YYzF55yhD6hluSyvBueM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJwYXBlcm1hcmsiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbWZ0c2lvL3BhcGVybWFyay9EWThjemNQSndjaTRLc0toSnE1YTNtczFmQks0IiwicHJldmlld1VybCI6InBhcGVybWFyay1naXQtZm9yay1hc2hpc2h2aXJhZGl5YTE1My0yMjgtZm9sZGVyY3JlYXRpb24tbWZ0c2lvLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicGFwZXJtYXJrLWdpdC1mb3JrLWFzaGlzaHZpcmFkaXlhMTUzLTIyOC1mb2xkZXJjcmVhdGlvbi1tZnRzaW8udmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **papermark** | ✅ Ready ([Inspect](https://vercel.com/mftsio/papermark/DY8czcPJwci4KsKhJq5a3ms1fBK4)) | [Visit Preview](https://papermark-git-fork-ashishviradiya153-228-foldercreation-mftsio.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/papermark-git-fork-ashishviradiya153-228-foldercreation-mftsio.vercel.app?via=pr-comment-feedback-link) | May 13, 2025 11:18am |


author_association: NONE