Skip to content

Commit da23967

Browse files
authored
Merge pull request #2936 from Bima42/feat/2931-template-bookmarking
feat: be able to bookmark templates
2 parents 964d79d + 0d5f452 commit da23967

8 files changed

Lines changed: 8423 additions & 10 deletions

File tree

apps/dokploy/components/dashboard/project/add-template.tsx

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
BookText,
3+
Bookmark,
34
CheckIcon,
45
ChevronsUpDown,
56
Globe,
@@ -82,6 +83,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
8283
const [open, setOpen] = useState(false);
8384
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
8485
const [selectedTags, setSelectedTags] = useState<string[]>([]);
86+
const [showBookmarksOnly, setShowBookmarksOnly] = useState(false);
8587
const [customBaseUrl, setCustomBaseUrl] = useState<string | undefined>(() => {
8688
// Try to get from props first, then localStorage
8789
if (baseUrl) return baseUrl;
@@ -122,8 +124,45 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
122124
enabled: open,
123125
},
124126
);
127+
128+
const { data: bookmarkIds = [], isLoading: isLoadingBookmarks } =
129+
api.user.getBookmarkedTemplates.useQuery(undefined, {
130+
enabled: open,
131+
});
132+
125133
const utils = api.useUtils();
126134

135+
const { mutateAsync: toggleBookmark } =
136+
api.user.toggleTemplateBookmark.useMutation({
137+
onMutate: async ({ templateId }) => {
138+
await utils.user.getBookmarkedTemplates.cancel();
139+
const previousBookmarks = utils.user.getBookmarkedTemplates.getData();
140+
141+
utils.user.getBookmarkedTemplates.setData(undefined, (old = []) => {
142+
if (old.includes(templateId)) {
143+
return old.filter((id) => id !== templateId);
144+
}
145+
return [...old, templateId];
146+
});
147+
148+
return { previousBookmarks };
149+
},
150+
onError: (err, variables, context) => {
151+
if (context?.previousBookmarks) {
152+
utils.user.getBookmarkedTemplates.setData(
153+
undefined,
154+
context.previousBookmarks,
155+
);
156+
}
157+
toast.error("Failed to update bookmark");
158+
},
159+
onSuccess: (data) => {
160+
toast.success(
161+
data.isBookmarked ? "Added to bookmarks" : "Removed from bookmarks",
162+
);
163+
},
164+
});
165+
127166
const [serverId, setServerId] = useState<string | undefined>(undefined);
128167
const { mutateAsync, isPending, error, isError } =
129168
api.compose.deployTemplate.useMutation();
@@ -137,7 +176,9 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
137176
query === "" ||
138177
template.name.toLowerCase().includes(query.toLowerCase()) ||
139178
template.description.toLowerCase().includes(query.toLowerCase());
140-
return matchesTags && matchesQuery;
179+
const matchesBookmarks =
180+
!showBookmarksOnly || bookmarkIds.includes(template.id);
181+
return matchesTags && matchesQuery && matchesBookmarks;
141182
}) || [];
142183

143184
const hasServers = servers && servers.length > 0;
@@ -146,6 +187,14 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
146187
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
147188
const shouldShowServerDropdown = hasServers;
148189

190+
const handleToggleBookmark = async (
191+
e: React.MouseEvent,
192+
templateId: string,
193+
) => {
194+
e.stopPropagation();
195+
await toggleBookmark({ templateId });
196+
};
197+
149198
return (
150199
<Dialog open={open} onOpenChange={setOpen}>
151200
<DialogTrigger className="w-full">
@@ -243,6 +292,20 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
243292
</Command>
244293
</PopoverContent>
245294
</Popover>
295+
<Button
296+
variant={showBookmarksOnly ? "default" : "outline"}
297+
size="icon"
298+
onClick={() => setShowBookmarksOnly(!showBookmarksOnly)}
299+
className="h-9 w-9 flex-shrink-0"
300+
disabled={isLoadingBookmarks}
301+
>
302+
<Bookmark
303+
className={cn(
304+
"size-4",
305+
showBookmarksOnly && "fill-current",
306+
)}
307+
/>
308+
</Button>
246309
<Button
247310
size="icon"
248311
onClick={() =>
@@ -299,11 +362,19 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
299362
</div>
300363
</div>
301364
) : templates.length === 0 ? (
302-
<div className="flex justify-center items-center w-full gap-2 min-h-[50vh]">
365+
<div className="flex flex-col justify-center items-center w-full gap-2 min-h-[50vh]">
303366
<SearchIcon className="text-muted-foreground size-6" />
304367
<div className="text-xl font-medium text-muted-foreground">
305-
No templates found
368+
{showBookmarksOnly
369+
? "No bookmarked templates found"
370+
: "No templates found"}
306371
</div>
372+
{showBookmarksOnly && (
373+
<p className="text-sm text-muted-foreground">
374+
Click the bookmark icon on templates to add them to
375+
bookmarks
376+
</p>
377+
)}
307378
</div>
308379
) : (
309380
<div
@@ -323,9 +394,25 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
323394
viewMode === "detailed" && "h-[400px]",
324395
)}
325396
>
326-
<Badge className="absolute top-2 right-2" variant="blue">
327-
{template?.version}
328-
</Badge>
397+
<div className="absolute top-2 left-2 z-10">
398+
<Button
399+
variant="ghost"
400+
size="icon"
401+
className="h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background"
402+
onClick={(e) => handleToggleBookmark(e, template.id)}
403+
>
404+
<Bookmark
405+
className={cn(
406+
"size-4",
407+
bookmarkIds.includes(template.id) &&
408+
"fill-yellow-400 text-yellow-400",
409+
)}
410+
/>
411+
</Button>
412+
</div>
413+
<div className="absolute top-2 right-2">
414+
<Badge variant="blue">{template?.version}</Badge>
415+
</div>
329416
<div
330417
className={cn(
331418
"flex-none p-6 pb-3 flex flex-col items-center gap-4 bg-muted/30",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "user" ADD COLUMN "bookmarkedTemplates" text[] DEFAULT ARRAY[]::text[];

0 commit comments

Comments
 (0)