From 2634cf9fe3f2b02535cb1265178f5a70fcc83d33 Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Thu, 20 Aug 2026 17:57:48 +0800 Subject: [PATCH 1/2] feat(settings): add api key copy button --- .../settings/components/ProviderApiConfig.vue | 23 ++++++++-- .../components/ProviderApiConfig.test.ts | 45 ++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/renderer/settings/components/ProviderApiConfig.vue b/src/renderer/settings/components/ProviderApiConfig.vue index a29263063..58cde85f6 100644 --- a/src/renderer/settings/components/ProviderApiConfig.vue +++ b/src/renderer/settings/components/ProviderApiConfig.vue @@ -122,9 +122,18 @@
- {{ maskedApiKey }} + {{ maskedApiKey }} +
{ showApiKey.value = false } +const handleKeyCopied = () => { + notifyRenderer({ + kind: 'success', + code: 'settings.provider.keyCopied', + title: t('common.copySuccess') + }) +} + const handleApiKeyChange = (value: string) => { emit('api-key-change', value) } diff --git a/test/renderer/components/ProviderApiConfig.test.ts b/test/renderer/components/ProviderApiConfig.test.ts index 38f3760d5..8db26719c 100644 --- a/test/renderer/components/ProviderApiConfig.test.ts +++ b/test/renderer/components/ProviderApiConfig.test.ts @@ -41,6 +41,19 @@ const buttonStub = defineComponent({ template: '' }) +const copyButtonStub = defineComponent({ + name: 'CopyButton', + inheritAttrs: false, + emits: ['copied', 'error'], + props: { + copyText: { + type: String, + default: '' + } + }, + template: '' +}) + const labelStub = defineComponent({ name: 'Label', inheritAttrs: false, @@ -114,7 +127,8 @@ async function setup(options?: { Input: createInputStub() })) vi.doMock('@dc-ui/components/button', () => ({ - DcButton: buttonStub + DcButton: buttonStub, + DcCopyButton: copyButtonStub })) vi.doMock('@shadcn/components/ui/label', () => ({ Label: labelStub @@ -416,6 +430,30 @@ describe('ProviderApiConfig', () => { expect((input.element as HTMLInputElement).value).toBe('') }) + it('renders a hover-revealed copy button in the masked key summary and copies on click', async () => { + const { wrapper, notifyRenderer } = await setup({ + provider: createProvider({ apiKey: 'sk-1234567890abcd' }) + }) + + const summary = wrapper.get('[data-testid="provider-api-key-summary"]') + const copyButton = wrapper.get('[data-testid="provider-copy-key-button"]') + + // The button lives inside the summary and is hidden until the row is hovered. + expect(summary.find('[data-testid="provider-copy-key-button"]').exists()).toBe(true) + expect(copyButton.classes()).toContain('opacity-0') + expect(copyButton.classes()).toContain('group-hover:opacity-100') + expect(wrapper.findComponent(copyButtonStub).props('copyText')).toBe('sk-1234567890abcd') + + await copyButton.trigger('click') + + expect(notifyRenderer).toHaveBeenCalledWith( + expect.objectContaining({ + kind: 'success', + title: 'common.copySuccess' + }) + ) + }) + it('keeps the stored key when the Update key editor is left empty', async () => { const { wrapper } = await setup({ provider: createProvider({ apiKey: 'sk-1234567890abcd' }) @@ -504,7 +542,10 @@ describe('ProviderApiConfig', () => { useModelCheckStore: () => ({ openDialog: vi.fn() }) })) vi.doMock('@shadcn/components/ui/input', () => ({ Input: createInputStub() })) - vi.doMock('@dc-ui/components/button', () => ({ DcButton: buttonStub })) + vi.doMock('@dc-ui/components/button', () => ({ + DcButton: buttonStub, + DcCopyButton: copyButtonStub + })) vi.doMock('@shadcn/components/ui/label', () => ({ Label: labelStub })) vi.doMock('@shadcn/components/ui/tooltip', () => ({ Tooltip: passthrough('Tooltip'), From 4541761142545a72a1ffaf4fb1efeaddd694627c Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Thu, 20 Aug 2026 18:20:28 +0800 Subject: [PATCH 2/2] fix(settings): align api key copy feedback --- .../settings/components/ProviderApiConfig.vue | 13 +++---------- .../components/ProviderApiConfig.test.ts | 19 +++++++------------ 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/renderer/settings/components/ProviderApiConfig.vue b/src/renderer/settings/components/ProviderApiConfig.vue index 58cde85f6..3564a1ffa 100644 --- a/src/renderer/settings/components/ProviderApiConfig.vue +++ b/src/renderer/settings/components/ProviderApiConfig.vue @@ -131,8 +131,7 @@ variant="ghost" size="icon-xs" :tooltip="t('common.copy')" - class="shrink-0 opacity-0 transition-opacity focus-visible:opacity-100 group-hover:opacity-100" - @copied="handleKeyCopied" + class="shrink-0 opacity-0 pointer-events-none transition-opacity duration-[var(--dc-motion-fast)] focus-visible:opacity-100 focus-visible:pointer-events-auto group-hover:opacity-100 group-hover:pointer-events-auto" />
!isEditingKey.value && Boolean(props.provider.apiKey?.trim())) const maskedApiKey = computed(() => { const key = props.provider.apiKey?.trim() ?? '' @@ -379,14 +380,6 @@ const startEditingKey = () => { showApiKey.value = false } -const handleKeyCopied = () => { - notifyRenderer({ - kind: 'success', - code: 'settings.provider.keyCopied', - title: t('common.copySuccess') - }) -} - const handleApiKeyChange = (value: string) => { emit('api-key-change', value) } diff --git a/test/renderer/components/ProviderApiConfig.test.ts b/test/renderer/components/ProviderApiConfig.test.ts index 8db26719c..a9842ecf0 100644 --- a/test/renderer/components/ProviderApiConfig.test.ts +++ b/test/renderer/components/ProviderApiConfig.test.ts @@ -430,28 +430,23 @@ describe('ProviderApiConfig', () => { expect((input.element as HTMLInputElement).value).toBe('') }) - it('renders a hover-revealed copy button in the masked key summary and copies on click', async () => { - const { wrapper, notifyRenderer } = await setup({ + it('renders a hover-revealed copy button in the masked key summary', async () => { + const { wrapper } = await setup({ provider: createProvider({ apiKey: 'sk-1234567890abcd' }) }) const summary = wrapper.get('[data-testid="provider-api-key-summary"]') const copyButton = wrapper.get('[data-testid="provider-copy-key-button"]') - // The button lives inside the summary and is hidden until the row is hovered. + // The button lives inside the summary and stays hidden until hovered or focused. expect(summary.find('[data-testid="provider-copy-key-button"]').exists()).toBe(true) expect(copyButton.classes()).toContain('opacity-0') + expect(copyButton.classes()).toContain('pointer-events-none') expect(copyButton.classes()).toContain('group-hover:opacity-100') + expect(copyButton.classes()).toContain('group-hover:pointer-events-auto') + expect(copyButton.classes()).toContain('focus-visible:opacity-100') + expect(copyButton.attributes('tooltip')).toBe('common.copy') expect(wrapper.findComponent(copyButtonStub).props('copyText')).toBe('sk-1234567890abcd') - - await copyButton.trigger('click') - - expect(notifyRenderer).toHaveBeenCalledWith( - expect.objectContaining({ - kind: 'success', - title: 'common.copySuccess' - }) - ) }) it('keeps the stored key when the Update key editor is left empty', async () => {