mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
fix: acknowledge desktop copy completion
Copy actions that did not use CopyButton could succeed silently, leaving users unsure whether the clipboard changed. Show localized success toasts for H5 URL, QR link, and workspace path copies, and report a generic copy failure when the shared clipboard helper cannot write. Constraint: Copy must keep working across macOS, Linux, and Windows Tauri WebViews, so the existing web Clipboard API plus textarea execCommand fallback remains the platform-neutral mechanism. Rejected: Add native OS-specific clipboard calls | unnecessary surface area when the current web fallback already covers the desktop WebView contexts. Confidence: high Scope-risk: narrow Directive: Copy surfaces that cannot visibly change their own button text should emit a toast on success or failure. Tested: cd desktop && bun run test -- --run src/__tests__/generalSettings.test.tsx src/components/workspace/WorkspacePanel.test.tsx src/i18n/index.test.tsx Tested: bun run check:desktop Tested: bun run verify (passed=10 failed=0 skipped=0) Not-tested: Manual OS matrix on Linux and Windows; behavior uses platform-neutral browser clipboard APIs inside Tauri.
This commit is contained in:
parent
9ddaf29b4e
commit
0b1eb2c5b8
@ -446,6 +446,10 @@ describe('Settings > General tab', () => {
|
||||
expect(clipboardMock.copyTextToClipboard).toHaveBeenCalledWith(
|
||||
'http://192.168.0.102:3456/?serverUrl=http%3A%2F%2F192.168.0.102%3A3456&h5Token=h5_default_generated_token',
|
||||
)
|
||||
expect(useUIStore.getState().toasts[useUIStore.getState().toasts.length - 1]).toMatchObject({
|
||||
type: 'success',
|
||||
message: 'QR link copied.',
|
||||
})
|
||||
})
|
||||
|
||||
it('guides enabled H5 users to generate a token before the QR code exists', async () => {
|
||||
@ -517,6 +521,10 @@ describe('Settings > General tab', () => {
|
||||
})
|
||||
|
||||
expect(clipboardMock.copyTextToClipboard).toHaveBeenCalledWith('https://phone.example/app')
|
||||
expect(useUIStore.getState().toasts[useUIStore.getState().toasts.length - 1]).toMatchObject({
|
||||
type: 'success',
|
||||
message: 'H5 URL copied.',
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the H5-specific store error when the H5 settings load failed', () => {
|
||||
|
||||
@ -245,6 +245,7 @@ import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useWorkspaceChatContextStore } from '../../stores/workspaceChatContextStore'
|
||||
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
|
||||
import { useUIStore } from '../../stores/uiStore'
|
||||
import { WorkspacePanel } from './WorkspacePanel'
|
||||
|
||||
describe('WorkspacePanel', () => {
|
||||
@ -1258,6 +1259,10 @@ describe('WorkspacePanel', () => {
|
||||
expect(execCommand).toHaveBeenCalledWith('copy')
|
||||
})
|
||||
expect(writeText).toHaveBeenCalledWith('/repo/src/App.tsx')
|
||||
expect(useUIStore.getState().toasts[useUIStore.getState().toasts.length - 1]).toMatchObject({
|
||||
type: 'success',
|
||||
message: 'Path copied.',
|
||||
})
|
||||
} finally {
|
||||
Object.defineProperty(document, 'execCommand', {
|
||||
configurable: true,
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
} from '../../stores/workspacePanelStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useWorkspaceChatContextStore } from '../../stores/workspaceChatContextStore'
|
||||
import { useUIStore } from '../../stores/uiStore'
|
||||
import { copyTextToClipboard } from '../chat/clipboard'
|
||||
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
|
||||
import {
|
||||
@ -888,6 +889,7 @@ function TreeNode({
|
||||
|
||||
export function WorkspacePanel({ sessionId }: WorkspacePanelProps) {
|
||||
const t = useTranslation()
|
||||
const addToast = useUIStore((state) => state.addToast)
|
||||
const [filterQuery, setFilterQuery] = useState('')
|
||||
const [isViewMenuOpen, setIsViewMenuOpen] = useState(false)
|
||||
const [previewTabContextMenu, setPreviewTabContextMenu] = useState<{ tabId: string; x: number; y: number } | null>(null)
|
||||
@ -1069,8 +1071,12 @@ export function WorkspacePanel({ sessionId }: WorkspacePanelProps) {
|
||||
|
||||
const copyWorkspacePath = async (path: string) => {
|
||||
const resolvedPath = resolveWorkspaceAttachmentPath(status?.workDir, path)
|
||||
await copyTextToClipboard(resolvedPath)
|
||||
const copied = await copyTextToClipboard(resolvedPath)
|
||||
setFileContextMenu(null)
|
||||
addToast({
|
||||
type: copied ? 'success' : 'error',
|
||||
message: copied ? t('workspace.pathCopied') : t('common.copyFailed'),
|
||||
})
|
||||
}
|
||||
|
||||
const renderChangedView = () => {
|
||||
|
||||
@ -14,6 +14,7 @@ export const en = {
|
||||
'common.enable': 'Enable',
|
||||
'common.disable': 'Disable',
|
||||
'common.active': 'ACTIVE',
|
||||
'common.copyFailed': 'Copy failed.',
|
||||
|
||||
// ─── Sidebar ──────────────────────────────────────
|
||||
'sidebar.newSession': 'New session',
|
||||
@ -92,6 +93,7 @@ export const en = {
|
||||
'workspace.addToChat': 'Add to chat',
|
||||
'workspace.addSelectionToChat': 'Add to chat',
|
||||
'workspace.copyPath': 'Copy path',
|
||||
'workspace.pathCopied': 'Path copied.',
|
||||
'workspace.localComment': 'Local comment',
|
||||
'workspace.commentLine': 'Comment line {line}',
|
||||
'workspace.commentLineTarget': 'Line {line}',
|
||||
@ -760,6 +762,8 @@ export const en = {
|
||||
'settings.general.h5AccessCopy': 'Copy',
|
||||
'settings.general.h5AccessCopyUrl': 'Copy H5 URL',
|
||||
'settings.general.h5AccessCopyLaunchUrl': 'Copy QR link',
|
||||
'settings.general.h5AccessUrlCopied': 'H5 URL copied.',
|
||||
'settings.general.h5AccessLaunchUrlCopied': 'QR link copied.',
|
||||
'settings.general.h5AccessHideToken': 'Hide token',
|
||||
'settings.general.h5AccessTokenNotAvailable': 'Regenerate to show a new token.',
|
||||
'settings.general.h5AccessPublicUrl': 'Public URL',
|
||||
|
||||
@ -16,6 +16,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'common.enable': '启用',
|
||||
'common.disable': '禁用',
|
||||
'common.active': '已激活',
|
||||
'common.copyFailed': '复制失败。',
|
||||
|
||||
// ─── Sidebar ──────────────────────────────────────
|
||||
'sidebar.newSession': '新建会话',
|
||||
@ -94,6 +95,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'workspace.addToChat': '添加到聊天',
|
||||
'workspace.addSelectionToChat': '添加至对话',
|
||||
'workspace.copyPath': '复制路径',
|
||||
'workspace.pathCopied': '路径已复制。',
|
||||
'workspace.localComment': '本地评论',
|
||||
'workspace.commentLine': '评论第 {line} 行',
|
||||
'workspace.commentLineTarget': '第 {line} 行',
|
||||
@ -762,6 +764,8 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.general.h5AccessCopy': '复制',
|
||||
'settings.general.h5AccessCopyUrl': '复制 H5 链接',
|
||||
'settings.general.h5AccessCopyLaunchUrl': '复制扫码链接',
|
||||
'settings.general.h5AccessUrlCopied': 'H5 链接已复制。',
|
||||
'settings.general.h5AccessLaunchUrlCopied': '扫码链接已复制。',
|
||||
'settings.general.h5AccessHideToken': '隐藏令牌',
|
||||
'settings.general.h5AccessTokenNotAvailable': '重新生成后可显示新的令牌。',
|
||||
'settings.general.h5AccessPublicUrl': '公开访问 URL',
|
||||
|
||||
@ -1835,6 +1835,7 @@ function H5AccessSettings() {
|
||||
updateH5AccessSettings,
|
||||
} = useSettingsStore()
|
||||
const t = useTranslation()
|
||||
const addToast = useUIStore((s) => s.addToast)
|
||||
const [h5PublicBaseUrlDraft, setH5PublicBaseUrlDraft] = useState(h5Access.publicBaseUrl ?? '')
|
||||
const [h5GeneratedToken, setH5GeneratedToken] = useState<string | null>(null)
|
||||
const [h5TokenVisible, setH5TokenVisible] = useState(false)
|
||||
@ -1895,12 +1896,20 @@ function H5AccessSettings() {
|
||||
|
||||
const handleH5UrlCopy = async () => {
|
||||
if (!h5AccessUrl) return
|
||||
await copyTextToClipboard(h5AccessUrl)
|
||||
const copied = await copyTextToClipboard(h5AccessUrl)
|
||||
addToast({
|
||||
type: copied ? 'success' : 'error',
|
||||
message: copied ? t('settings.general.h5AccessUrlCopied') : t('common.copyFailed'),
|
||||
})
|
||||
}
|
||||
|
||||
const handleH5LaunchUrlCopy = async () => {
|
||||
if (!h5LaunchUrl) return
|
||||
await copyTextToClipboard(h5LaunchUrl)
|
||||
const copied = await copyTextToClipboard(h5LaunchUrl)
|
||||
addToast({
|
||||
type: copied ? 'success' : 'error',
|
||||
message: copied ? t('settings.general.h5AccessLaunchUrlCopied') : t('common.copyFailed'),
|
||||
})
|
||||
}
|
||||
|
||||
const handleH5EnableConfirm = async () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user