Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions packages/app/e2e/sidebar/sidebar-project-actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { test, expect } from "../fixtures"
import { openSidebar, withSession, clickMenuItem } from "../actions"
import { pawworkSidebarSelector, dropdownMenuContentSelector } from "../selectors"
import type { TestInfo } from "@playwright/test"

async function capture(page: any, testInfo: TestInfo, name: string) {
await page.screenshot({
path: testInfo.outputPath(`${name}.png`),
fullPage: true,
})
}

test("project group can be renamed from sidebar", async ({ page, sdk, gotoSession }, testInfo) => {
const stamp = Date.now()
await withSession(sdk, `rename project ${stamp}`, async (a) => {
await withSession(sdk, `rename project b ${stamp}`, async () => {
await gotoSession(a.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()

// Switch to project sort so group headers exist.
await sidebar.locator('[data-action="pawwork-sort-trigger"]').click()
await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()

const header = sidebar.locator('[data-action="pawwork-group-toggle"]').first()
await expect(header).toBeVisible()

// Screenshot: project group header with overflow button
await capture(page, testInfo, `01-project-header-${stamp}`)

// Open the project menu via overflow button
const menuTrigger = sidebar.locator('[data-action="project-row-menu"]').first()
await expect(menuTrigger).toBeVisible()
await menuTrigger.click()

// Screenshot: project menu opened
await capture(page, testInfo, `02-project-menu-${stamp}`)

// Click rename
const menu = page.locator(dropdownMenuContentSelector).first()
await clickMenuItem(menu, /Rename project/)

// Dialog should appear
const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Rename project/ }).first()
await expect(dialog).toBeVisible()

// Screenshot: rename dialog
await capture(page, testInfo, `03-rename-dialog-${stamp}`)

// Type new name
const input = dialog.locator('input').first()
await input.fill(`Renamed Project ${stamp}`)

// Save
const saveButton = dialog.locator('button').filter({ hasText: /Save/ }).first()
await saveButton.click()

// Dialog should close
await expect(dialog).not.toBeVisible()

// Screenshot: after rename
await capture(page, testInfo, `04-after-rename-${stamp}`)

// Note: project rename updates ProjectMeta, which may not reflect immediately in sidebar
// without a refresh. The screenshot above captures the post-rename state.
})
})
})

test("project group can be removed from sidebar", async ({ page, sdk, gotoSession }, testInfo) => {
const stamp = Date.now()
await withSession(sdk, `remove project ${stamp}`, async (a) => {
await withSession(sdk, `remove project b ${stamp}`, async () => {
await gotoSession(a.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()

// Switch to project sort so group headers exist.
await sidebar.locator('[data-action="pawwork-sort-trigger"]').click()
await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()

// Count initial groups
const initialGroups = sidebar.locator('[data-action="pawwork-group-toggle"]')
const initialCount = await initialGroups.count()
expect(initialCount).toBeGreaterThan(0)

// Open the project menu via overflow button
const menuTrigger = sidebar.locator('[data-action="project-row-menu"]').first()
await expect(menuTrigger).toBeVisible()
await menuTrigger.click()

// Screenshot: remove menu
await capture(page, testInfo, `05-remove-menu-${stamp}`)

// Click remove
const menu = page.locator(dropdownMenuContentSelector).first()
await clickMenuItem(menu, /Remove from sidebar/)

// Confirm dialog should appear
const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Remove project from sidebar/ }).first()
await expect(dialog).toBeVisible()

// Screenshot: remove confirm dialog
await capture(page, testInfo, `06-remove-dialog-${stamp}`)

// Confirm removal
const confirmButton = dialog.locator('button').filter({ hasText: /Remove/ }).first()
await confirmButton.click()

// Dialog should close
await expect(dialog).not.toBeVisible()

// Screenshot: after remove (toast may appear)
await capture(page, testInfo, `07-after-remove-${stamp}`)

// Group count should decrease
const remainingGroups = sidebar.locator('[data-action="pawwork-group-toggle"]')
await expect.poll(async () => await remainingGroups.count()).toBeLessThan(initialCount)
})
})
})

test("hidden project restores when session is opened", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
await withSession(sdk, `restore hidden ${stamp}`, async (a) => {
await gotoSession(a.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()

// Switch to project sort
await sidebar.locator('[data-action="pawwork-sort-trigger"]').click()
await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()

// Remove the project
const menuTrigger = sidebar.locator('[data-action="project-row-menu"]').first()
await menuTrigger.click()

const menu = page.locator(dropdownMenuContentSelector).first()
await clickMenuItem(menu, /Remove from sidebar/)

const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Remove project from sidebar/ }).first()
await expect(dialog).toBeVisible()
await dialog.locator('button').filter({ hasText: /Remove/ }).first().click()

// Group should be hidden
const groups = sidebar.locator('[data-action="pawwork-group-toggle"]')
const countAfterRemove = await groups.count()

// Now open the session again (this should restore the project)
await gotoSession(a.id)
await openSidebar(page)

// Re-locate sidebar after navigation
const sidebarAfter = page.locator(pawworkSidebarSelector).first()

// Switch to project sort again
await sidebarAfter.locator('[data-action="pawwork-sort-trigger"]').click()
await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()

// Group should be back
const groupsAfter = sidebarAfter.locator('[data-action="pawwork-group-toggle"]')
await expect.poll(async () => await groupsAfter.count()).toBeGreaterThan(countAfterRemove)
})
})
46 changes: 46 additions & 0 deletions packages/app/src/components/dialog-remove-project.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Dialog } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createSignal } from "solid-js"
import { useLanguage } from "@/context/language"

export function DialogRemoveProject(props: {
name: string
onConfirm: () => Promise<void> | void
}) {
const language = useLanguage()
const dialog = useDialog()
const [removing, setRemoving] = createSignal(false)

const handleRemove = async () => {
if (removing()) return
setRemoving(true)
try {
await props.onConfirm()
dialog.close()
} finally {
setRemoving(false)
}
}

return (
<Dialog title={language.t("project.remove.title")} fit class="w-full max-w-[420px] mx-auto">
<div class="px-6 pt-2 pb-6">
<span class="text-13-regular text-fg-strong">
{language.t("project.remove.confirm", { name: props.name })}
</span>
<p class="mt-2 text-13-regular text-fg-weak">
{language.t("project.remove.description")}
</p>
</div>
<div class="flex justify-end gap-2 px-6 pb-6">
<Button variant="secondary" onClick={() => dialog.close()} disabled={removing()}>
{language.t("common.cancel")}
</Button>
<Button variant="danger" onClick={handleRemove} disabled={removing()}>
{language.t("common.remove")}
</Button>
</div>
</Dialog>
)
}
76 changes: 76 additions & 0 deletions packages/app/src/components/dialog-rename-project.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createSignal } from "solid-js"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { TextField } from "@opencode-ai/ui/text-field"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"

export function DialogRenameProject(props: {
name: string
onConfirm: (name: string) => Promise<void> | void
}) {
const language = useLanguage()
const dialog = useDialog()
const [value, setValue] = createSignal(props.name)
const [saving, setSaving] = createSignal(false)

const handleSave = async () => {
const next = value().trim()
if (!next || saving()) return
setSaving(true)
try {
await props.onConfirm(next)
dialog.close()
} catch {
showToast({
title: language.t("toast.project.rename.failed.title"),
description: language.t("toast.project.rename.failed.description"),
variant: "error",
})
} finally {
setSaving(false)
}
}

return (
<Dialog title={language.t("project.rename")} fit class="w-full max-w-[420px] mx-auto">
<div class="px-6 pt-2 pb-6">
<TextField
aria-label={language.t("project.rename")}
autofocus
value={value()}
onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
setValue(e.currentTarget.value)
}
onKeyDown={(e: KeyboardEvent & { currentTarget: HTMLInputElement }) => {
if (e.key === "Enter") {
e.preventDefault()
void handleSave()
}
if (e.key === "Escape") {
e.preventDefault()
if (saving()) return
dialog.close()
}
}}
onFocus={(e: FocusEvent & { currentTarget: HTMLInputElement }) =>
e.currentTarget.select()
}
/>
</div>
<div class="flex justify-end gap-2 px-6 pb-6">
<Button variant="secondary" onClick={() => dialog.close()} disabled={saving()}>
{language.t("common.cancel")}
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={saving() || !value().trim()}
>
{language.t("common.save")}
</Button>
</div>
</Dialog>
)
}
12 changes: 12 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,8 @@ export const dict = {
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
"toast.session.rename.failed.title": "Failed to rename session",
"toast.session.rename.failed.description": "Try again, or close the dialog to keep the current name.",
"toast.project.rename.failed.title": "Failed to rename project",
"toast.project.rename.failed.description": "Try again, or close the dialog to keep the current name.",

"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
Expand Down Expand Up @@ -765,6 +767,8 @@ export const dict = {
"common.rename": "Rename",
"common.reset": "Reset",
"common.delete": "Delete",
"common.remove": "Remove",
"common.undo": "Undo",
"common.close": "Close",
"common.edit": "Edit",
"common.loadMore": "Load more",
Expand Down Expand Up @@ -815,6 +819,14 @@ export const dict = {
"sidebar.pawwork.sort.optionByProject": "By project",
"sidebar.pawwork.searchHistory": "Use Search for older sessions",

"project.rename": "Rename project",
Comment thread
Astro-Han marked this conversation as resolved.
"project.remove": "Remove from sidebar",
"project.remove.title": "Remove project from sidebar",
"project.remove.confirm": "Remove \"{{name}}\" from sidebar?",
"project.remove.description": "Project files and sessions will remain on disk. Re-open this project to restore it to the sidebar.",
"project.remove.toast.title": "Project removed from sidebar",
"project.remove.toast.description": "You can restore it by re-opening the project.",

"debugBar.ariaLabel": "Development performance diagnostics",
"debugBar.na": "n/a",
"debugBar.nav.label": "NAV",
Expand Down
12 changes: 12 additions & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@ export const dict = {
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
"toast.session.rename.failed.title": "重命名失败",
"toast.session.rename.failed.description": "请重试,或关闭对话框保持当前名称。",
"toast.project.rename.failed.title": "重命名项目失败",
"toast.project.rename.failed.description": "请重试,或关闭对话框保持当前名称。",
"toast.session.listFailed.title": "无法加载 {{project}} 的会话",
"toast.update.title": "有可用更新",
"toast.update.description": "爪印有新版本 ({{version}}) 可安装。",
Expand Down Expand Up @@ -697,6 +699,8 @@ export const dict = {
"common.rename": "重命名",
"common.reset": "重置",
"common.delete": "删除",
"common.remove": "移除",
"common.undo": "撤销",
"common.close": "关闭",
"common.edit": "编辑",
"common.loadMore": "加载更多",
Expand Down Expand Up @@ -727,6 +731,14 @@ export const dict = {
"sidebar.pawwork.sort.optionByProject": "按项目",
"sidebar.pawwork.searchHistory": "搜索历史会话",

"project.rename": "重命名项目",
Comment thread
Astro-Han marked this conversation as resolved.
"project.remove": "从侧边栏移除",
"project.remove.title": "从侧边栏移除项目",
"project.remove.confirm": "从侧边栏移除「{{name}}」?",
"project.remove.description": "项目文件和会话仍会保留在磁盘上。重新打开该项目即可恢复显示。",
"project.remove.toast.title": "项目已从侧边栏移除",
"project.remove.toast.description": "重新打开该项目可恢复显示。",

"app.name.desktop": "爪印",

"settings.section.desktop": "桌面",
Expand Down
Loading
Loading