mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
fix(desktop): open native folder dialog when creating projects
This commit is contained in:
@@ -1,11 +1,30 @@
|
||||
import '@testing-library/jest-dom'
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useState } from 'react'
|
||||
|
||||
const nativeDialog = vi.hoisted(() => ({
|
||||
isDesktop: false,
|
||||
open: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/desktopHost', () => ({
|
||||
getDesktopHost: () => ({
|
||||
isDesktop: nativeDialog.isDesktop,
|
||||
capabilities: { dialogs: true },
|
||||
dialogs: { open: nativeDialog.open },
|
||||
}),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
nativeDialog.isDesktop = false
|
||||
nativeDialog.open.mockReset()
|
||||
})
|
||||
|
||||
vi.mock('../../i18n', () => ({
|
||||
useTranslation: () => (key: string, params?: Record<string, string | number>) => {
|
||||
const translations: Record<string, string> = {
|
||||
'dirPicker.chooseProjectFolder': 'Choose project folder',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.save': 'Save',
|
||||
'sidebar.projectEditor.createTitle': 'Create project',
|
||||
@@ -71,6 +90,40 @@ function CreateHarness({
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('ProjectEditorModal', () => {
|
||||
it('opens the native folder dialog directly and preserves the selected path', async () => {
|
||||
nativeDialog.isDesktop = true
|
||||
nativeDialog.open.mockResolvedValue('/workspace/selected ')
|
||||
const onSourceFolderChange = vi.fn()
|
||||
const onSubmit = vi.fn()
|
||||
render(<CreateHarness onSubmit={onSubmit} onSourceFolderChange={onSourceFolderChange} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Choose source folder' })).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Choose project folder' }))
|
||||
await waitFor(() => expect(onSourceFolderChange).toHaveBeenCalledWith('/workspace/selected '))
|
||||
expect(nativeDialog.open).toHaveBeenCalledWith({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Choose project folder',
|
||||
})
|
||||
expect(screen.getByText('/workspace/selected')).toBeInTheDocument()
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves the source unchanged on cancellation and reports dialog failures', async () => {
|
||||
nativeDialog.isDesktop = true
|
||||
nativeDialog.open.mockResolvedValueOnce(null).mockRejectedValueOnce(new Error('Dialog unavailable'))
|
||||
const onSourceFolderChange = vi.fn()
|
||||
render(<CreateHarness onSubmit={vi.fn()} onSourceFolderChange={onSourceFolderChange} />)
|
||||
|
||||
const choose = screen.getByRole('button', { name: 'Choose project folder' })
|
||||
fireEvent.click(choose)
|
||||
await waitFor(() => expect(choose).not.toBeDisabled())
|
||||
expect(onSourceFolderChange).not.toHaveBeenCalled()
|
||||
fireEvent.click(choose)
|
||||
await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('Dialog unavailable'))
|
||||
expect(onSourceFolderChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('validates normalized names and preserves exact source and logical paths', async () => {
|
||||
const onSubmit = vi.fn()
|
||||
const onSourceFolderChange = vi.fn()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
|
||||
import { FolderOpen } from 'lucide-react'
|
||||
import { getDesktopHost } from '@/lib/desktopHost'
|
||||
import { DirectoryPicker } from '@/components/composite/DirectoryPicker'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
@@ -66,6 +68,8 @@ export function ProjectEditorModal(props: ProjectEditorModalProps) {
|
||||
onClose,
|
||||
onSubmit,
|
||||
} = props
|
||||
const host = getDesktopHost()
|
||||
const useNativeFolderDialog = host.isDesktop && host.capabilities.dialogs
|
||||
const sourceFolder = mode === 'create' ? props.sourceFolder : logicalRoot ?? ''
|
||||
const suggestedFolderName = suggestedName?.trim() || folderName(logicalRoot || sourceFolder)
|
||||
const [name, setName] = useState(() => initialName ?? suggestedFolderName)
|
||||
@@ -155,6 +159,20 @@ export function ProjectEditorModal(props: ProjectEditorModalProps) {
|
||||
}))
|
||||
}
|
||||
|
||||
const handleChooseSourceFolder = () => {
|
||||
if (mode !== 'create') return
|
||||
void runAction(async () => {
|
||||
const selected = await host.dialogs.open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: t('dirPicker.chooseProjectFolder'),
|
||||
})
|
||||
if (typeof selected === 'string' && selected.length > 0) {
|
||||
props.onSourceFolderChange(selected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestoreFolderName = () => {
|
||||
if (mode !== 'edit' || !props.onRestoreFolderName) return
|
||||
void runAction(props.onRestoreFolderName, () => {
|
||||
@@ -220,7 +238,21 @@ export function ProjectEditorModal(props: ProjectEditorModalProps) {
|
||||
{t('sidebar.projectEditor.sourceFolder')}
|
||||
<span className="ml-0.5 text-[var(--color-error)]">*</span>
|
||||
</span>
|
||||
<DirectoryPicker value={sourceFolder} onChange={props.onSourceFolderChange} variant="workbar" />
|
||||
{useNativeFolderDialog ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleChooseSourceFolder}
|
||||
disabled={busy}
|
||||
title={sourceFolder || undefined}
|
||||
aria-label={t('dirPicker.chooseProjectFolder')}
|
||||
className="min-w-0 self-start max-w-full"
|
||||
>
|
||||
<FolderOpen size={16} className="shrink-0" aria-hidden="true" />
|
||||
<span className="truncate">{sourceFolder || t('dirPicker.chooseProjectFolder')}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<DirectoryPicker value={sourceFolder} onChange={props.onSourceFolderChange} variant="workbar" />
|
||||
)}
|
||||
{showValidation && sourceFolderError && (
|
||||
<p id="project-editor-source-folder-error" role="alert" className="text-xs text-[var(--color-error)]">
|
||||
{sourceFolderError}
|
||||
|
||||
Reference in New Issue
Block a user