Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 19 additions & 0 deletions docs/features/data-settings-danger-zone-entry/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Implementation Plan

## UI

- Replace the three always-visible Danger Zone destructive buttons in `DataSettings.vue` with one outline reset entry.
- Keep the existing confirmation dialog and radio choices as the authoritative place for choosing the reset type.
- Add stable test IDs for the reset entry and dialog choices.

## Behavior

- Opening the reset dialog resets the selected type to `chat`.
- Confirmation continues to call `devicePresenter.resetDataByType(resetType.value)`.
- Existing disabled states for import and backup still gate both the entry and confirmation action.

## Validation

- Update focused renderer tests for the single entry and dialog options.
- Verify selected reset type still reaches the presenter.
- Run format, i18n, lint, and the focused Data Settings test.
19 changes: 19 additions & 0 deletions docs/features/data-settings-danger-zone-entry/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Data Settings Danger Zone Entry

## User Story

用户在数据设置页查看常规数据操作时,Danger Zone 不应以三个高权重红色按钮长期占据页面注意力;只有主动进入重置流程后,才需要看到具体的破坏性重置类型。

## Acceptance Criteria

- Danger Zone 默认只显示一个低噪声的重置入口。
- 重置入口使用 outline/destructive text 风格,不使用大面积红色背景。
- 具体重置类型仍在确认弹窗中选择,包含聊天、知识库、配置和完全重置。
- 默认重置类型为聊天数据。
- 现有重置调用语义保持不变。

## Non-goals

- 不改变任何重置数据语义。
- 不新增 IPC、Presenter 方法或持久化格式。
- 不调整 YoBrowser、数据库修复、模型配置更新等相邻操作。
7 changes: 7 additions & 0 deletions docs/features/data-settings-danger-zone-entry/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Tasks

- [x] Add SDD artifacts.
- [x] Replace the Danger Zone outer button group with one reset entry.
- [x] Keep dialog reset choices visible and testable.
- [x] Update renderer coverage for layout and reset behavior.
- [x] Run validation commands.
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[tools]
node = "24.14.1"
pnpm = "10.13.1"
pnpm = "10.33.4"
78 changes: 34 additions & 44 deletions src/renderer/settings/components/DataSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -496,44 +496,18 @@
</div>
</div>
<AlertDialog v-model:open="isResetDialogOpen">
<div class="grid w-full shrink-0 gap-2 lg:w-[42rem] lg:grid-cols-3">
<Button
variant="destructive"
class="h-auto min-h-12 w-full whitespace-normal px-3 py-2"
:disabled="isResetActionDisabled"
:dir="languageStore.dir"
@click="openResetDialog('chat')"
>
<Icon icon="lucide:rotate-ccw" class="h-4 w-4 shrink-0" />
<span class="min-w-0 text-center text-sm font-medium leading-tight">{{
t('settings.data.resetChatData')
}}</span>
</Button>
<Button
variant="destructive"
class="h-auto min-h-12 w-full whitespace-normal px-3 py-2"
:disabled="isResetActionDisabled"
:dir="languageStore.dir"
@click="openResetDialog('knowledge')"
>
<Icon icon="lucide:book-x" class="h-4 w-4 shrink-0" />
<span class="min-w-0 text-center text-sm font-medium leading-tight">{{
t('settings.data.resetKnowledgeData')
}}</span>
</Button>
<Button
variant="destructive"
class="h-auto min-h-12 w-full whitespace-normal px-3 py-2"
:disabled="isResetActionDisabled"
:dir="languageStore.dir"
@click="openResetDialog('all')"
>
<Icon icon="lucide:triangle-alert" class="h-4 w-4 shrink-0" />
<span class="min-w-0 text-center text-sm font-medium leading-tight">{{
t('settings.data.resetAll')
}}</span>
</Button>
</div>
<Button
variant="outline"
class="w-full shrink-0 justify-center border-destructive/30 text-destructive hover:bg-destructive/10 hover:text-destructive lg:w-40"
:disabled="isResetActionDisabled"
:dir="languageStore.dir"
data-testid="danger-zone-reset-entry"
aria-haspopup="dialog"
@click="openResetDialog"
>
<Icon icon="lucide:triangle-alert" class="h-4 w-4" />
<span class="text-sm font-medium">{{ t('settings.data.resetData') }}</span>
</Button>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ t('settings.data.resetConfirmTitle') }}</AlertDialogTitle>
Expand All @@ -544,7 +518,9 @@
<div class="p-4">
<RadioGroup v-model="resetType" class="flex flex-col gap-3">
<div
class="flex items-start space-x-3 rounded-lg p-2 -m-2 hover:bg-accent"
class="-m-2 flex cursor-pointer items-start space-x-3 rounded-lg border border-transparent p-2 transition-colors hover:bg-accent"
:class="resetType === 'chat' ? 'border-destructive/25 bg-destructive/5' : ''"
data-testid="danger-zone-reset-option-chat"
@click="resetType = 'chat'"
>
<RadioGroupItem value="chat" id="reset-chat" class="mt-1" />
Expand All @@ -558,7 +534,11 @@
</div>
</div>
<div
class="flex items-start space-x-3 rounded-lg p-2 -m-2 hover:bg-accent"
class="-m-2 flex cursor-pointer items-start space-x-3 rounded-lg border border-transparent p-2 transition-colors hover:bg-accent"
:class="
resetType === 'knowledge' ? 'border-destructive/25 bg-destructive/5' : ''
"
data-testid="danger-zone-reset-option-knowledge"
@click="resetType = 'knowledge'"
>
<RadioGroupItem value="knowledge" id="reset-knowledge" class="mt-1" />
Expand All @@ -572,7 +552,11 @@
</div>
</div>
<div
class="flex items-start space-x-3 rounded-lg p-2 -m-2 hover:bg-accent"
class="-m-2 flex cursor-pointer items-start space-x-3 rounded-lg border border-transparent p-2 transition-colors hover:bg-accent"
:class="
resetType === 'config' ? 'border-destructive/25 bg-destructive/5' : ''
"
data-testid="danger-zone-reset-option-config"
@click="resetType = 'config'"
>
<RadioGroupItem value="config" id="reset-config" class="mt-1" />
Expand All @@ -586,7 +570,9 @@
</div>
</div>
<div
class="flex items-start space-x-3 rounded-lg p-2 -m-2 hover:bg-accent"
class="-m-2 flex cursor-pointer items-start space-x-3 rounded-lg border border-transparent p-2 transition-colors hover:bg-accent"
:class="resetType === 'all' ? 'border-destructive/25 bg-destructive/5' : ''"
data-testid="danger-zone-reset-option-all"
@click="resetType = 'all'"
>
<RadioGroupItem value="all" id="reset-all" class="mt-1" />
Expand Down Expand Up @@ -1449,8 +1435,12 @@ const closeResetDialog = () => {
resetType.value = 'chat'
}

const openResetDialog = (type: 'chat' | 'knowledge' | 'all') => {
resetType.value = type
const openResetDialog = () => {
if (isResetActionDisabled.value) {
return
}

resetType.value = 'chat'
isResetDialogOpen.value = true
}

Expand Down
57 changes: 37 additions & 20 deletions test/renderer/components/DataSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,8 @@ const findRefreshButton = (wrapper: ReturnType<typeof mount>) =>
const findRepairButton = (wrapper: ReturnType<typeof mount>) =>
findButtonByText(wrapper, 'settings.data.databaseRepair', 'Repair database')

const findResetAllButton = (wrapper: ReturnType<typeof mount>) =>
findButtonByText(wrapper, 'settings.data.resetAll', 'Reset all data')

const findResetKnowledgeButton = (wrapper: ReturnType<typeof mount>) =>
findButtonByText(wrapper, 'settings.data.resetKnowledgeData', 'Reset knowledge data')

const findResetChatButton = (wrapper: ReturnType<typeof mount>) =>
findButtonByText(wrapper, 'settings.data.resetChatData', 'Reset chat data')
const findResetEntryButton = (wrapper: ReturnType<typeof mount>) =>
findButtonByText(wrapper, 'settings.data.resetData', 'Reset data')

const findResetConfirmButton = (wrapper: ReturnType<typeof mount>) =>
findButtonByText(wrapper, 'settings.data.confirmReset', 'Reset confirm')
Expand Down Expand Up @@ -307,22 +301,24 @@ describe('DataSettings', () => {
expect(wrapper.text()).toContain('settings.data.dangerZone.title')
expect(wrapper.text()).toContain('settings.data.resetChatData')
expect(wrapper.text()).toContain('settings.data.resetKnowledgeData')
expect(wrapper.text()).toContain('settings.data.resetConfig')
expect(wrapper.text()).toContain('settings.data.resetAll')
expect(wrapper.text()).toContain('settings.data.yoBrowser.title')
expect(wrapper.text()).toContain('settings.data.databaseEncryption.systemCredentialStore')
})

it('keeps long danger zone labels within taller wrapping buttons', async () => {
it('renders a quiet danger zone entry and keeps reset choices in the dialog', async () => {
const { wrapper } = await setup()

const resetButtons = [findResetChatButton(wrapper), findResetKnowledgeButton(wrapper)]
const resetEntry = findResetEntryButton(wrapper)

for (const button of resetButtons) {
expect(button.classes()).toContain('min-h-12')
expect(button.classes()).toContain('whitespace-normal')
expect(button.find('span').classes()).toContain('min-w-0')
expect(button.find('span').classes()).toContain('leading-tight')
}
expect(resetEntry.attributes('variant')).toBe('outline')
expect(resetEntry.classes()).toContain('text-destructive')
expect(resetEntry.classes()).toContain('border-destructive/30')
expect(wrapper.find('[data-testid="danger-zone-reset-option-chat"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="danger-zone-reset-option-knowledge"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="danger-zone-reset-option-config"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="danger-zone-reset-option-all"]').exists()).toBe(true)
})

it('updates privacy mode from the data settings page', async () => {
Expand Down Expand Up @@ -588,9 +584,8 @@ describe('DataSettings', () => {
syncStore.syncEnabled = false
await nextTick()

expect(findResetChatButton(wrapper).attributes('disabled')).toBeUndefined()
expect(findResetKnowledgeButton(wrapper).attributes('disabled')).toBeUndefined()
expect(findResetAllButton(wrapper).attributes('disabled')).toBeUndefined()
expect(findResetEntryButton(wrapper).attributes('disabled')).toBeUndefined()
expect(findResetConfirmButton(wrapper).attributes('disabled')).toBeUndefined()
})

it('disables reset actions during import and blocks the reset handler', async () => {
Expand All @@ -599,12 +594,34 @@ describe('DataSettings', () => {
syncStore.isImporting = true
await nextTick()

expect(findResetAllButton(wrapper).attributes('disabled')).toBeDefined()
expect(findResetEntryButton(wrapper).attributes('disabled')).toBeDefined()
expect(findResetConfirmButton(wrapper).attributes('disabled')).toBeDefined()

findResetConfirmButton(wrapper).vm.$emit('click')
await flushPromises()

expect(presenterMocks.devicePresenter.resetDataByType).not.toHaveBeenCalled()
})

it('defaults reset type to chat when opening the reset dialog', async () => {
const { wrapper, presenterMocks } = await setup()

await wrapper.find('[data-testid="danger-zone-reset-option-all"]').trigger('click')
await findResetEntryButton(wrapper).trigger('click')
await findResetConfirmButton(wrapper).trigger('click')
await flushPromises()

expect(presenterMocks.devicePresenter.resetDataByType).toHaveBeenCalledWith('chat')
})

it('calls resetDataByType with the selected dialog reset type', async () => {
const { wrapper, presenterMocks } = await setup()

await findResetEntryButton(wrapper).trigger('click')
await wrapper.find('[data-testid="danger-zone-reset-option-knowledge"]').trigger('click')
await findResetConfirmButton(wrapper).trigger('click')
await flushPromises()

expect(presenterMocks.devicePresenter.resetDataByType).toHaveBeenCalledWith('knowledge')
})
})