Unverified 提交 d01ef2e9 authored 作者: wwwillchen-bot's avatar wwwillchen-bot 提交者: GitHub

Move favorite button from app list to app details page (#2704)

## Summary - Move the favorite toggle button from the app list sidebar to the app details page header - Update AppItem component to show a small filled star indicator for favorited apps (instead of an interactive button) - Simplify AppList by removing favorite-related props and hooks - Update E2E tests to test favorite functionality from the app details page ## Test plan - Open an app's details page and verify the favorite button appears next to the app name - Click the favorite button and verify the star fills in (app is favorited) - Click again to unfavorite and verify the star becomes unfilled - Return to the app list and verify favorited apps show a small filled star indicator - Run E2E tests: `npx playwright test favorite_app.spec.ts` 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2704" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com> Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 da660b2a
......@@ -2,7 +2,7 @@ import { test, Timeout } from "./helpers/test_helper";
import { expect } from "@playwright/test";
test.describe("Favorite App Tests", () => {
test("Add app to favorite", async ({ po }) => {
test("Add app to favorite from app details", async ({ po }) => {
await po.setUp({ autoApprove: true });
// Create a test app
......@@ -13,32 +13,29 @@ test.describe("Favorite App Tests", () => {
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 — hover first like a real user would,
// then wait for the app to finish starting before the click resolves.
const favoriteButton = appItem
.locator("xpath=..")
.locator('[data-testid="favorite-button"]');
// Click on the app to go to app details
await firstAppItem.click();
// Wait for app details page to load
const appDetailsPage = po.page.getByTestId("app-details-page");
await expect(appDetailsPage).toBeVisible({ timeout: Timeout.MEDIUM });
// Click the favorite button in app details
const favoriteButton = appDetailsPage.locator(
'[data-testid="favorite-button"]',
);
await expect(favoriteButton).toBeVisible();
await appItem.hover();
await favoriteButton.click();
// Check that the star is filled (favorited).
// Use a longer timeout because the addToFavorite IPC call may be waiting
// for the app startup lock to release.
// Check that the star is filled (favorited)
const star = favoriteButton.locator("svg");
await expect(star).toHaveClass(/(?:^|\s)fill-\[#6c55dc\]/, {
timeout: Timeout.MEDIUM,
});
});
test("Remove app from favorite", async ({ po }) => {
test("Remove app from favorite from app details", async ({ po }) => {
await po.setUp({ autoApprove: true });
// Create a test app
......@@ -49,17 +46,18 @@ test.describe("Favorite App Tests", () => {
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}"]`);
// Click on the app to go to app details
await firstAppItem.click();
// Wait for app details page to load
const appDetailsPage = po.page.getByTestId("app-details-page");
await expect(appDetailsPage).toBeVisible({ timeout: Timeout.MEDIUM });
// First, add to favorite
const favoriteButton = appItem
.locator("xpath=..")
.locator('[data-testid="favorite-button"]');
await appItem.hover();
const favoriteButton = appDetailsPage.locator(
'[data-testid="favorite-button"]',
);
await favoriteButton.click();
// Check that the star is filled (favorited)
......@@ -69,15 +67,9 @@ test.describe("Favorite App Tests", () => {
});
// Now, remove from favorite
const unfavoriteButton = appItem
.locator("xpath=..")
.locator('[data-testid="favorite-button"]');
await expect(unfavoriteButton).toBeVisible();
await appItem.hover();
await unfavoriteButton.click();
await favoriteButton.click();
// Check that the star is not filled (unfavorited)
// Match fill-[#6c55dc] only at start or after whitespace (not as part of hover:fill-...)
await expect(star).not.toHaveClass(/(?:^|\s)fill-\[#6c55dc\]/, {
timeout: Timeout.MEDIUM,
});
......
{
"name": "dyad",
"version": "0.37.0-beta.1",
"version": "0.37.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dyad",
"version": "0.37.0-beta.1",
"version": "0.37.0",
"license": "MIT",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.46",
......
......@@ -13,15 +13,12 @@ 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);
......@@ -66,11 +63,6 @@ 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 (
<>
<SidebarGroup
......@@ -113,16 +105,20 @@ export function AppList({ show }: { show?: boolean }) {
) : (
<SidebarMenu className="space-y-1" data-testid="app-list">
<SidebarGroupLabel>Favorite apps</SidebarGroupLabel>
{favoriteApps.map((app) => (
<AppItem
key={app.id}
app={app}
handleAppClick={handleAppClick}
selectedAppId={selectedAppId}
handleToggleFavorite={handleToggleFavorite}
isFavoriteLoading={isFavoriteLoading}
/>
))}
{favoriteApps.length === 0 ? (
<div className="px-4 text-xs text-gray-500 italic">
Star an app from its details page to pin it here
</div>
) : (
favoriteApps.map((app) => (
<AppItem
key={app.id}
app={app}
handleAppClick={handleAppClick}
selectedAppId={selectedAppId}
/>
))
)}
<SidebarGroupLabel>Other apps</SidebarGroupLabel>
{nonFavoriteApps.map((app) => (
<AppItem
......@@ -130,8 +126,6 @@ export function AppList({ show }: { show?: boolean }) {
app={app}
handleAppClick={handleAppClick}
selectedAppId={selectedAppId}
handleToggleFavorite={handleToggleFavorite}
isFavoriteLoading={isFavoriteLoading}
/>
))}
</SidebarMenu>
......
......@@ -8,17 +8,9 @@ type AppItemProps = {
app: ListedApp;
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) {
export function AppItem({ app, handleAppClick, selectedAppId }: AppItemProps) {
return (
<SidebarMenuItem className="mb-1 relative ">
<div className="flex w-[206px] items-center" title={app.name}>
......@@ -33,7 +25,15 @@ export function AppItem({
data-testid={`app-list-item-${app.name}`}
>
<div className="flex flex-col w-4/5">
<span className="truncate">{app.name}</span>
<div className="flex items-center gap-1">
<span className="truncate">{app.name}</span>
{app.isFavorite && (
<Star
size={12}
className="fill-[#6c55dc] text-[#6c55dc] flex-shrink-0"
/>
)}
</div>
<span className="text-xs text-gray-500">
{formatDistanceToNow(new Date(app.createdAt), {
addSuffix: true,
......@@ -41,26 +41,6 @@ export function AppItem({
</span>
</div>
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleToggleFavorite(app.id, e)}
disabled={isFavoriteLoading}
className="absolute top-1 right-1 p-1 mx-1 h-6 w-6 z-10"
key={app.id}
data-testid="favorite-button"
>
<Star
size={12}
className={
app.isFavorite
? "fill-[#6c55dc] text-[#6c55dc]"
: selectedAppId === app.id
? "hover:fill-black hover:text-black"
: "hover:fill-[#6c55dc] hover:stroke-[#6c55dc] hover:text-[#6c55dc]"
}
/>
</Button>
</div>
</SidebarMenuItem>
);
......
......@@ -12,6 +12,7 @@ import {
MessageCircle,
Pencil,
Folder,
Star,
} from "lucide-react";
import {
Popover,
......@@ -44,6 +45,7 @@ import { useCheckName } from "@/hooks/useCheckName";
import { AppUpgrades } from "@/components/AppUpgrades";
import { CapacitorControls } from "@/components/CapacitorControls";
import { GithubCollaboratorManager } from "@/components/GithubCollaboratorManager";
import { useAddAppToFavorite } from "@/hooks/useAddAppToFavorite";
export default function AppDetailsPage() {
const navigate = useNavigate();
......@@ -75,6 +77,8 @@ export default function AppDetailsPage() {
debouncedNewCopyAppName,
);
const nameExists = checkNameResult?.exists ?? false;
const { toggleFavorite, isLoading: isFavoriteLoading } =
useAddAppToFavorite();
// Get the appId from search params and find the corresponding app
const appId = search.appId ? Number(search.appId) : null;
......@@ -284,6 +288,33 @@ export default function AppDetailsPage() {
<div className="w-full max-w-2xl mx-auto mt-10 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm relative">
<div className="flex items-center mb-3">
<h2 className="text-2xl font-bold">{selectedApp.name}</h2>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="sm"
className="ml-1 p-0.5 h-auto"
onClick={() => appId && toggleFavorite(appId)}
disabled={isFavoriteLoading}
data-testid="favorite-button"
/>
}
>
<Star
className={`h-4 w-4 ${
selectedApp.isFavorite
? "fill-[#6c55dc] text-[#6c55dc]"
: "hover:fill-[#6c55dc] hover:text-[#6c55dc]"
}`}
/>
</TooltipTrigger>
<TooltipContent>
{selectedApp.isFavorite
? "Remove from favorites"
: "Add to favorites"}
</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="sm"
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论