Skip to content

Commit 31960fc

Browse files
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 <[email protected]>
1 parent 99413db commit 31960fc

File tree

10 files changed

+101
-31
lines changed

10 files changed

+101
-31
lines changed

apps/web/components/dashboard/preview/AttachmentBox.tsx

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -101,43 +101,52 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
101101
prefetch={false}
102102
>
103103
{ASSET_TYPE_TO_ICON[asset.assetType]}
104-
<p>{humanFriendlyNameForAssertType(asset.assetType)}</p>
104+
<p>
105+
{asset.assetType === "userUploaded" && asset.fileName
106+
? asset.fileName
107+
: humanFriendlyNameForAssertType(asset.assetType)}
108+
</p>
105109
</Link>
106110
<div className="flex gap-2">
107111
<Link
108112
title="Download"
109113
target="_blank"
110114
href={getAssetUrl(asset.id)}
111115
className="flex items-center gap-1"
112-
download={humanFriendlyNameForAssertType(asset.assetType)}
116+
download={
117+
asset.assetType === "userUploaded" && asset.fileName
118+
? asset.fileName
119+
: humanFriendlyNameForAssertType(asset.assetType)
120+
}
113121
prefetch={false}
114122
>
115123
<Download className="size-4" />
116124
</Link>
117-
{isAllowedToAttachAsset(asset.assetType) && (
118-
<FilePickerButton
119-
title="Replace"
120-
loading={isReplacing}
121-
accept=".jgp,.JPG,.jpeg,.png,.webp"
122-
multiple={false}
123-
variant="none"
124-
size="none"
125-
className="flex items-center gap-2"
126-
onFileSelect={(file) =>
127-
uploadAsset(file, {
128-
onSuccess: (resp) => {
129-
replaceAsset({
130-
bookmarkId: bookmark.id,
131-
oldAssetId: asset.id,
132-
newAssetId: resp.assetId,
133-
});
134-
},
135-
})
136-
}
137-
>
138-
<Pencil className="size-4" />
139-
</FilePickerButton>
140-
)}
125+
{isAllowedToAttachAsset(asset.assetType) &&
126+
asset.assetType !== "userUploaded" && (
127+
<FilePickerButton
128+
title="Replace"
129+
loading={isReplacing}
130+
accept=".jgp,.JPG,.jpeg,.png,.webp"
131+
multiple={false}
132+
variant="none"
133+
size="none"
134+
className="flex items-center gap-2"
135+
onFileSelect={(file) =>
136+
uploadAsset(file, {
137+
onSuccess: (resp) => {
138+
replaceAsset({
139+
bookmarkId: bookmark.id,
140+
oldAssetId: asset.id,
141+
newAssetId: resp.assetId,
142+
});
143+
},
144+
})
145+
}
146+
>
147+
<Pencil className="size-4" />
148+
</FilePickerButton>
149+
)}
141150
{isAllowedToDetachAsset(asset.assetType) && (
142151
<ActionConfirmingDialog
143152
title="Delete Attachment?"
@@ -194,6 +203,30 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
194203
Attach a Banner
195204
</FilePickerButton>
196205
)}
206+
<FilePickerButton
207+
title="Upload File"
208+
loading={isAttaching}
209+
multiple={false}
210+
variant="ghost"
211+
size="none"
212+
className="flex w-full items-center justify-center gap-2"
213+
onFileSelect={(file) =>
214+
uploadAsset(file, {
215+
onSuccess: (resp) => {
216+
attachAsset({
217+
bookmarkId: bookmark.id,
218+
asset: {
219+
id: resp.assetId,
220+
assetType: "userUploaded",
221+
},
222+
});
223+
},
224+
})
225+
}
226+
>
227+
<Plus className="size-4" />
228+
Upload File
229+
</FilePickerButton>
197230
</CollapsibleContent>
198231
</Collapsible>
199232
);

apps/web/lib/attachments.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
FileCode,
55
Image,
66
Paperclip,
7+
Upload,
78
Video,
89
} from "lucide-react";
910

@@ -18,5 +19,6 @@ export const ASSET_TYPE_TO_ICON: Record<ZAssetType, React.ReactNode> = {
1819
video: <Video className="size-4" />,
1920
bookmarkAsset: <Paperclip className="size-4" />,
2021
linkHtmlContent: <FileCode className="size-4" />,
22+
userUploaded: <Upload className="size-4" />,
2123
unknown: <Paperclip className="size-4" />,
2224
};

packages/db/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ export const enum AssetTypes {
228228
LINK_VIDEO = "linkVideo",
229229
LINK_HTML_CONTENT = "linkHtmlContent",
230230
BOOKMARK_ASSET = "bookmarkAsset",
231+
USER_UPLOADED = "userUploaded",
231232
UNKNOWN = "unknown",
232233
}
233234

@@ -246,6 +247,7 @@ export const assets = sqliteTable(
246247
AssetTypes.LINK_VIDEO,
247248
AssetTypes.LINK_HTML_CONTENT,
248249
AssetTypes.BOOKMARK_ASSET,
250+
AssetTypes.USER_UPLOADED,
249251
AssetTypes.UNKNOWN,
250252
],
251253
}).notNull(),

packages/open-api/karakeep-openapi-spec.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,13 @@
317317
"video",
318318
"bookmarkAsset",
319319
"precrawledArchive",
320+
"userUploaded",
320321
"unknown"
321322
]
323+
},
324+
"fileName": {
325+
"type": "string",
326+
"nullable": true
322327
}
323328
},
324329
"required": [
@@ -1703,6 +1708,7 @@
17031708
"video",
17041709
"bookmarkAsset",
17051710
"precrawledArchive",
1711+
"userUploaded",
17061712
"unknown"
17071713
]
17081714
}
@@ -1737,8 +1743,13 @@
17371743
"video",
17381744
"bookmarkAsset",
17391745
"precrawledArchive",
1746+
"userUploaded",
17401747
"unknown"
17411748
]
1749+
},
1750+
"fileName": {
1751+
"type": "string",
1752+
"nullable": true
17421753
}
17431754
},
17441755
"required": [

packages/open-api/lib/bookmarks.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { z } from "zod";
66

77
import {
88
zAssetSchema,
9+
zAssetTypesSchema,
910
zBareBookmarkSchema,
1011
zManipulatedTagSchema,
1112
zNewBookmarkRequestSchema,
@@ -418,7 +419,10 @@ registry.registerPath({
418419
description: "The asset to attach",
419420
content: {
420421
"application/json": {
421-
schema: zAssetSchema,
422+
schema: z.object({
423+
id: z.string(),
424+
assetType: zAssetTypesSchema,
425+
}),
422426
},
423427
},
424428
},

packages/shared/types/bookmarks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ export const zAssetTypesSchema = z.enum([
2424
"video",
2525
"bookmarkAsset",
2626
"precrawledArchive",
27+
"userUploaded",
2728
"unknown",
2829
]);
2930
export type ZAssetType = z.infer<typeof zAssetTypesSchema>;
3031

3132
export const zAssetSchema = z.object({
3233
id: z.string(),
3334
assetType: zAssetTypesSchema,
35+
fileName: z.string().nullish(),
3436
});
3537

3638
export const zBookmarkedLinkSchema = z.object({

packages/trpc/lib/attachments.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function mapDBAssetTypeToUserType(assetType: AssetTypes): ZAssetType {
1616
[AssetTypes.LINK_VIDEO]: "video",
1717
[AssetTypes.LINK_HTML_CONTENT]: "linkHtmlContent",
1818
[AssetTypes.BOOKMARK_ASSET]: "bookmarkAsset",
19+
[AssetTypes.USER_UPLOADED]: "userUploaded",
1920
[AssetTypes.UNKNOWN]: "bannerImage",
2021
};
2122
return map[assetType];
@@ -33,6 +34,7 @@ export function mapSchemaAssetTypeToDB(
3334
video: AssetTypes.LINK_VIDEO,
3435
bookmarkAsset: AssetTypes.BOOKMARK_ASSET,
3536
linkHtmlContent: AssetTypes.LINK_HTML_CONTENT,
37+
userUploaded: AssetTypes.USER_UPLOADED,
3638
unknown: AssetTypes.UNKNOWN,
3739
};
3840
return map[assetType];
@@ -48,6 +50,7 @@ export function humanFriendlyNameForAssertType(type: ZAssetType) {
4850
video: "Video",
4951
bookmarkAsset: "Bookmark Asset",
5052
linkHtmlContent: "HTML Content",
53+
userUploaded: "User Uploaded File",
5154
unknown: "Unknown",
5255
};
5356
return map[type];
@@ -63,6 +66,7 @@ export function isAllowedToAttachAsset(type: ZAssetType) {
6366
video: true,
6467
bookmarkAsset: false,
6568
linkHtmlContent: false,
69+
userUploaded: true,
6670
unknown: false,
6771
};
6872
return map[type];
@@ -78,6 +82,7 @@ export function isAllowedToDetachAsset(type: ZAssetType) {
7882
video: true,
7983
bookmarkAsset: false,
8084
linkHtmlContent: false,
85+
userUploaded: true,
8186
unknown: false,
8287
};
8388
return map[type];

packages/trpc/models/bookmarks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ export class Bookmark implements PrivacyAware {
295295
acc[bookmarkId].assets.push({
296296
id: row.assets.id,
297297
assetType: mapDBAssetTypeToUserType(row.assets.assetType),
298+
fileName: row.assets.fileName,
298299
});
299300
}
300301

packages/trpc/routers/assets.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ export const assetsAppRouter = router({
9999
.input(
100100
z.object({
101101
bookmarkId: z.string(),
102-
asset: zAssetSchema,
102+
asset: z.object({
103+
id: z.string(),
104+
assetType: zAssetTypesSchema,
105+
}),
103106
}),
104107
)
105108
.output(zAssetSchema)
@@ -112,16 +115,22 @@ export const assetsAppRouter = router({
112115
message: "You can't attach this type of asset",
113116
});
114117
}
115-
await ctx.db
118+
const [updatedAsset] = await ctx.db
116119
.update(assets)
117120
.set({
118121
assetType: mapSchemaAssetTypeToDB(input.asset.assetType),
119122
bookmarkId: input.bookmarkId,
120123
})
121124
.where(
122125
and(eq(assets.id, input.asset.id), eq(assets.userId, ctx.user.id)),
123-
);
124-
return input.asset;
126+
)
127+
.returning();
128+
129+
return {
130+
id: updatedAsset.id,
131+
assetType: mapDBAssetTypeToUserType(updatedAsset.assetType),
132+
fileName: updatedAsset.fileName,
133+
};
125134
}),
126135
replaceAsset: authedProcedure
127136
.input(

packages/trpc/routers/bookmarks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ async function toZodSchema(
253253
assets: assets.map((a) => ({
254254
id: a.id,
255255
assetType: mapDBAssetTypeToUserType(a.assetType),
256+
fileName: a.fileName,
256257
})),
257258
...rest,
258259
};

0 commit comments

Comments
 (0)