diff --git a/drizzle/0013_damp_mephistopheles.sql b/drizzle/0013_damp_mephistopheles.sql new file mode 100644 index 0000000..73815ca --- /dev/null +++ b/drizzle/0013_damp_mephistopheles.sql @@ -0,0 +1 @@ +ALTER TABLE `apps` ADD `is_favorite` integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0013_snapshot.json b/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..9b97594 --- /dev/null +++ b/drizzle/meta/0013_snapshot.json @@ -0,0 +1,739 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3c245790-42ce-4d19-9d9d-51ed1a022a7a", + "prevId": "8c77d7f5-9f88-4186-8aff-8385e060e59f", + "tables": { + "apps": { + "name": "apps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_branch": { + "name": "github_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supabase_project_id": { + "name": "supabase_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_project_id": { + "name": "neon_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_development_branch_id": { + "name": "neon_development_branch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_preview_branch_id": { + "name": "neon_preview_branch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_project_id": { + "name": "vercel_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_project_name": { + "name": "vercel_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_team_id": { + "name": "vercel_team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_deployment_url": { + "name": "vercel_deployment_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "install_command": { + "name": "install_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_command": { + "name": "start_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_context": { + "name": "chat_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_favorite": { + "name": "is_favorite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "0" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initial_commit_hash": { + "name": "initial_commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_app_id_apps_id_fk": { + "name": "chats_app_id_apps_id_fk", + "tableFrom": "chats", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_model_providers": { + "name": "language_model_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_models": { + "name": "language_models", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_name": { + "name": "api_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "builtin_provider_id": { + "name": "builtin_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_provider_id": { + "name": "custom_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "language_models_custom_provider_id_language_model_providers_id_fk": { + "name": "language_models_custom_provider_id_language_model_providers_id_fk", + "tableFrom": "language_models", + "tableTo": "language_model_providers", + "columnsFrom": [ + "custom_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_servers": { + "name": "mcp_servers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "args": { + "name": "args", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_json": { + "name": "env_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "0" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_tool_consents": { + "name": "mcp_tool_consents", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "server_id": { + "name": "server_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consent": { + "name": "consent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ask'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "uniq_mcp_consent": { + "name": "uniq_mcp_consent", + "columns": [ + "server_id", + "tool_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "mcp_tool_consents_server_id_mcp_servers_id_fk": { + "name": "mcp_tool_consents_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_tool_consents", + "tableTo": "mcp_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prompts": { + "name": "prompts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "versions": { + "name": "versions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "neon_db_timestamp": { + "name": "neon_db_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "versions_app_commit_unique": { + "name": "versions_app_commit_unique", + "columns": [ + "app_id", + "commit_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "versions_app_id_apps_id_fk": { + "name": "versions_app_id_apps_id_fk", + "tableFrom": "versions", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index dc2fbb2..de4495f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1758320228637, "tag": "0012_bouncy_fenris", "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1759068733234, + "tag": "0013_damp_mephistopheles", + "breakpoints": true } ] } \ No newline at end of file diff --git a/e2e-tests/favorite_app.spec.ts b/e2e-tests/favorite_app.spec.ts new file mode 100644 index 0000000..0296cda --- /dev/null +++ b/e2e-tests/favorite_app.spec.ts @@ -0,0 +1,72 @@ +import { test } from "./helpers/test_helper"; +import { expect } from "@playwright/test"; + +test.describe("Favorite App Tests", () => { + test("Add app to favorite", async ({ po }) => { + await po.setUp({ autoApprove: true }); + + // Create a test app + await po.sendPrompt("create a test app"); + await po.goToAppsTab(); + + // Get the app name from the UI (randomly generated) + const appItems = await po.page.getByTestId(/^app-list-item-/).all(); + expect(appItems.length).toBeGreaterThan(0); + const firstAppItem = appItems[0]; + const testId = await firstAppItem.getAttribute("data-testid"); + const appName = testId!.replace("app-list-item-", ""); + + // Get the app item (assuming it's not favorited initially) + const appItem = po.page.locator(`[data-testid="app-list-item-${appName}"]`); + await expect(appItem).toBeVisible(); + + // Click the favorite button + const favoriteButton = appItem + .locator("xpath=..") + .locator('[data-testid="favorite-button"]'); + await expect(favoriteButton).toBeVisible(); + await favoriteButton.click(); + + // Check that the star is filled (favorited) + const star = favoriteButton.locator("svg"); + await expect(star).toHaveClass(/fill-\[#6c55dc\]/); + }); + + test("Remove app from favorite", async ({ po }) => { + await po.setUp({ autoApprove: true }); + + // Create a test app + await po.sendPrompt("create a test app"); + await po.goToAppsTab(); + + // Get the app name from the UI + const appItems = await po.page.getByTestId(/^app-list-item-/).all(); + expect(appItems.length).toBeGreaterThan(0); + const firstAppItem = appItems[0]; + const testId = await firstAppItem.getAttribute("data-testid"); + const appName = testId!.replace("app-list-item-", ""); + + // Get the app item + const appItem = po.page.locator(`[data-testid="app-list-item-${appName}"]`); + + // First, add to favorite + const favoriteButton = appItem + .locator("xpath=..") + .locator('[data-testid="favorite-button"]'); + await favoriteButton.click(); + + // Check that the star is filled (favorited) + const star = favoriteButton.locator("svg"); + await expect(star).toHaveClass(/fill-\[#6c55dc\]/); + + // Now, remove from favorite + const unfavoriteButton = appItem + .locator("xpath=..") + .locator('[data-testid="favorite-button"]'); + await expect(unfavoriteButton).toBeVisible(); + await unfavoriteButton.click(); + + // Check that the star is not filled (unfavorited) + await expect(star).not.toHaveClass(/fill-\[#6c55dc\]/); + }); +}); diff --git a/src/components/AppList.tsx b/src/components/AppList.tsx index ef24da2..a1c3f9c 100644 --- a/src/components/AppList.tsx +++ b/src/components/AppList.tsx @@ -1,5 +1,4 @@ import { useNavigate } from "@tanstack/react-router"; -import { formatDistanceToNow } from "date-fns"; import { PlusCircle, Search } from "lucide-react"; import { useAtom, useSetAtom } from "jotai"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; @@ -8,19 +7,21 @@ import { SidebarGroupContent, SidebarGroupLabel, SidebarMenu, - SidebarMenuItem, } from "@/components/ui/sidebar"; import { Button } from "@/components/ui/button"; import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { useLoadApps } from "@/hooks/useLoadApps"; import { useMemo, useState } from "react"; import { AppSearchDialog } from "./AppSearchDialog"; - +import { useAddAppToFavorite } from "@/hooks/useAddAppToFavorite"; +import { AppItem } from "./appItem"; export function AppList({ show }: { show?: boolean }) { const navigate = useNavigate(); const [selectedAppId, setSelectedAppId] = useAtom(selectedAppIdAtom); const setSelectedChatId = useSetAtom(selectedChatIdAtom); const { apps, loading, error } = useLoadApps(); + const { toggleFavorite, isLoading: isFavoriteLoading } = + useAddAppToFavorite(); // search dialog state const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(false); @@ -35,6 +36,17 @@ export function AppList({ show }: { show?: boolean }) { })), [apps], ); + + const favoriteApps = useMemo( + () => apps.filter((app) => app.isFavorite), + [apps], + ); + + const nonFavoriteApps = useMemo( + () => apps.filter((app) => !app.isFavorite), + [apps], + ); + if (!show) { return null; } @@ -54,6 +66,11 @@ export function AppList({ show }: { show?: boolean }) { // We'll eventually need a create app workflow }; + const handleToggleFavorite = (appId: number, e: React.MouseEvent) => { + e.stopPropagation(); + toggleFavorite(appId); + }; + return ( <> ) : ( - {apps.map((app) => ( - - - + Favorite apps + {favoriteApps.map((app) => ( + + ))} + Other apps + {nonFavoriteApps.map((app) => ( + ))} )} diff --git a/src/components/appItem.tsx b/src/components/appItem.tsx new file mode 100644 index 0000000..9864617 --- /dev/null +++ b/src/components/appItem.tsx @@ -0,0 +1,67 @@ +import { formatDistanceToNow } from "date-fns"; +import { Star } from "lucide-react"; +import { SidebarMenuItem } from "@/components/ui/sidebar"; +import { Button } from "@/components/ui/button"; +import { App } from "@/ipc/ipc_types"; + +type AppItemProps = { + app: App; + handleAppClick: (id: number) => void; + selectedAppId: number | null; + handleToggleFavorite: (appId: number, e: React.MouseEvent) => void; + isFavoriteLoading: boolean; +}; + +export function AppItem({ + app, + handleAppClick, + selectedAppId, + handleToggleFavorite, + isFavoriteLoading, +}: AppItemProps) { + return ( + +
+ + +
+
+ ); +} diff --git a/src/db/schema.ts b/src/db/schema.ts index f841a4c..4717b08 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -39,6 +39,9 @@ export const apps = sqliteTable("apps", { installCommand: text("install_command"), startCommand: text("start_command"), chatContext: text("chat_context", { mode: "json" }), + isFavorite: integer("is_favorite", { mode: "boolean" }) + .notNull() + .default(sql`0`), }); export const chats = sqliteTable("chats", { diff --git a/src/hooks/useAddAppToFavorite.ts b/src/hooks/useAddAppToFavorite.ts new file mode 100644 index 0000000..bb3ed38 --- /dev/null +++ b/src/hooks/useAddAppToFavorite.ts @@ -0,0 +1,36 @@ +import { useMutation } from "@tanstack/react-query"; +import { IpcClient } from "@/ipc/ipc_client"; +import { showError, showSuccess } from "@/lib/toast"; +import { useAtom } from "jotai"; +import { appsListAtom } from "@/atoms/appAtoms"; + +export function useAddAppToFavorite() { + const [_, setApps] = useAtom(appsListAtom); + + const mutation = useMutation({ + mutationFn: async (appId: number): Promise => { + const result = await IpcClient.getInstance().addAppToFavorite(appId); + return result.isFavorite; + }, + onSuccess: (newIsFavorite, appId) => { + setApps((currentApps) => + currentApps.map((app) => + app.id === appId ? { ...app, isFavorite: newIsFavorite } : app, + ), + ); + showSuccess("App favorite status updated"); + }, + onError: (error) => { + showError(error.message || "Failed to update favorite status"); + }, + }); + + return { + toggleFavorite: mutation.mutate, + toggleFavoriteAsync: mutation.mutateAsync, + isLoading: mutation.isPending, + error: mutation.error, + isError: mutation.isError, + isSuccess: mutation.isSuccess, + }; +} diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts index f02bc88..f28f966 100644 --- a/src/ipc/handlers/app_handlers.ts +++ b/src/ipc/handlers/app_handlers.ts @@ -1075,6 +1075,53 @@ export function registerAppHandlers() { }, ); + ipcMain.handle( + "add-to-favorite", + async ( + _, + { appId }: { appId: number }, + ): Promise<{ isFavorite: boolean }> => { + return withLock(appId, async () => { + try { + // Fetch the current isFavorite value + const result = await db + .select({ isFavorite: apps.isFavorite }) + .from(apps) + .where(eq(apps.id, appId)) + .limit(1); + + if (result.length === 0) { + throw new Error(`App with ID ${appId} not found.`); + } + + const currentIsFavorite = result[0].isFavorite; + + // Toggle the isFavorite value + const updated = await db + .update(apps) + .set({ isFavorite: !currentIsFavorite }) + .where(eq(apps.id, appId)) + .returning({ isFavorite: apps.isFavorite }); + + if (updated.length === 0) { + throw new Error( + `Failed to update favorite status for app ID ${appId}.`, + ); + } + + // Return the updated isFavorite value + return { isFavorite: updated[0].isFavorite }; + } catch (error: any) { + logger.error( + `Error in add-to-favorite handler for app ID ${appId}:`, + error, + ); + throw new Error(`Failed to toggle favorite status: ${error.message}`); + } + }); + }, + ); + ipcMain.handle( "rename-app", async ( diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 8be2f60..49f3df7 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -274,6 +274,20 @@ export class IpcClient { return this.ipcRenderer.invoke("get-app", appId); } + public async addAppToFavorite( + appId: number, + ): Promise<{ isFavorite: boolean }> { + try { + const result = await this.ipcRenderer.invoke("add-to-favorite", { + appId, + }); + return result; + } catch (error) { + showError(error); + throw error; + } + } + public async getAppEnvVars( params: GetAppEnvVarsParams, ): Promise<{ key: string; value: string }[]> { diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index d603618..ff6c596 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -99,6 +99,7 @@ export interface App { vercelDeploymentUrl: string | null; installCommand: string | null; startCommand: string | null; + isFavorite: boolean; } export interface Version { diff --git a/src/preload.ts b/src/preload.ts index 23d82bd..1907c93 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -127,6 +127,8 @@ const validInvokeChannels = [ "prompts:create", "prompts:update", "prompts:delete", + // adding app to favorite + "add-to-favorite", // Test-only channels // These should ALWAYS be guarded with IS_TEST_BUILD in the main process. // We can't detect with IS_TEST_BUILD in the preload script because