From 31960fcd11daa2dfaf8ae409c80b572c9b22940f Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sat, 8 Nov 2025 15:22:14 +0000
Subject: feat: Add support for user uploaded files (#2100)

* feat: add user file upload support for bookmarks

Add a new "user-uploaded" asset type that allows users to upload and
attach their own files to bookmarks from the attachment box in the
bookmark preview page.

Changes:
- Add USER_UPLOADED asset type to database schema
- Add userUploaded to zAssetTypesSchema for type safety
- Update attachment permissions to allow attaching/detaching user files
- Add fileName field to asset schema for displaying custom filenames
- Add "Upload File" button in AttachmentBox component
- Display actual filename for user-uploaded files
- Allow any file type for user uploads (respects existing upload limits)
- Add Upload icon for user-uploaded files

Fixes #1863 related asset attachment improvements

* fix: ensure fileName is returned and remove edit button for user uploads

- Fix attachAsset mutation to fetch and return complete asset with fileName
  instead of just returning the input (which lacks fileName)
- Remove replace/edit button for user-uploaded files - users can only
  delete and re-upload instead
- This ensures the filename displays correctly in the UI immediately
  after upload

Fixes fileName propagation issue for user-uploaded assets

* fix asset file name

* remove filename from attach asset api

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 .../components/dashboard/preview/AttachmentBox.tsx | 85 +++++++++++++++-------
 1 file changed, 59 insertions(+), 26 deletions(-)

(limited to 'apps/web/components/dashboard/preview/AttachmentBox.tsx')

diff --git a/apps/web/components/dashboard/preview/AttachmentBox.tsx b/apps/web/components/dashboard/preview/AttachmentBox.tsx
index 87c56eb8..e24cc646 100644
--- a/apps/web/components/dashboard/preview/AttachmentBox.tsx
+++ b/apps/web/components/dashboard/preview/AttachmentBox.tsx
@@ -101,7 +101,11 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
               prefetch={false}
             >
               {ASSET_TYPE_TO_ICON[asset.assetType]}
-              <p>{humanFriendlyNameForAssertType(asset.assetType)}</p>
+              <p>
+                {asset.assetType === "userUploaded" && asset.fileName
+                  ? asset.fileName
+                  : humanFriendlyNameForAssertType(asset.assetType)}
+              </p>
             </Link>
             <div className="flex gap-2">
               <Link
@@ -109,35 +113,40 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
                 target="_blank"
                 href={getAssetUrl(asset.id)}
                 className="flex items-center gap-1"
-                download={humanFriendlyNameForAssertType(asset.assetType)}
+                download={
+                  asset.assetType === "userUploaded" && asset.fileName
+                    ? asset.fileName
+                    : humanFriendlyNameForAssertType(asset.assetType)
+                }
                 prefetch={false}
               >
                 <Download className="size-4" />
               </Link>
-              {isAllowedToAttachAsset(asset.assetType) && (
-                <FilePickerButton
-                  title="Replace"
-                  loading={isReplacing}
-                  accept=".jgp,.JPG,.jpeg,.png,.webp"
-                  multiple={false}
-                  variant="none"
-                  size="none"
-                  className="flex items-center gap-2"
-                  onFileSelect={(file) =>
-                    uploadAsset(file, {
-                      onSuccess: (resp) => {
-                        replaceAsset({
-                          bookmarkId: bookmark.id,
-                          oldAssetId: asset.id,
-                          newAssetId: resp.assetId,
-                        });
-                      },
-                    })
-                  }
-                >
-                  <Pencil className="size-4" />
-                </FilePickerButton>
-              )}
+              {isAllowedToAttachAsset(asset.assetType) &&
+                asset.assetType !== "userUploaded" && (
+                  <FilePickerButton
+                    title="Replace"
+                    loading={isReplacing}
+                    accept=".jgp,.JPG,.jpeg,.png,.webp"
+                    multiple={false}
+                    variant="none"
+                    size="none"
+                    className="flex items-center gap-2"
+                    onFileSelect={(file) =>
+                      uploadAsset(file, {
+                        onSuccess: (resp) => {
+                          replaceAsset({
+                            bookmarkId: bookmark.id,
+                            oldAssetId: asset.id,
+                            newAssetId: resp.assetId,
+                          });
+                        },
+                      })
+                    }
+                  >
+                    <Pencil className="size-4" />
+                  </FilePickerButton>
+                )}
               {isAllowedToDetachAsset(asset.assetType) && (
                 <ActionConfirmingDialog
                   title="Delete Attachment?"
@@ -194,6 +203,30 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
               Attach a Banner
             </FilePickerButton>
           )}
+        <FilePickerButton
+          title="Upload File"
+          loading={isAttaching}
+          multiple={false}
+          variant="ghost"
+          size="none"
+          className="flex w-full items-center justify-center gap-2"
+          onFileSelect={(file) =>
+            uploadAsset(file, {
+              onSuccess: (resp) => {
+                attachAsset({
+                  bookmarkId: bookmark.id,
+                  asset: {
+                    id: resp.assetId,
+                    assetType: "userUploaded",
+                  },
+                });
+              },
+            })
+          }
+        >
+          <Plus className="size-4" />
+          Upload File
+        </FilePickerButton>
       </CollapsibleContent>
     </Collapsible>
   );
-- 
cgit v1.2.3-70-g09d2

