Skip to content

Commit 831c6d3

Browse files
authored
Merge pull request #275 from kygo8/cursor/parity-sync-overwrite-confirm-93f4
Ask before Folder Sync overwrite and delete
2 parents 15efcfb + f9138e6 commit 831c6d3

3 files changed

Lines changed: 217 additions & 10 deletions

File tree

src/views/FolderSyncView.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ describe('FolderSyncView', () => {
170170
expect(wrapper.find('[data-testid="folder-sync-run"]').attributes('disabled')).toBeUndefined()
171171
await wrapper.find('[data-testid="folder-sync-accept"]').trigger('click')
172172
await wrapper.find('[data-testid="folder-sync-run"]').trigger('click')
173+
expect(executeFolderSync).not.toHaveBeenCalled()
174+
expect(wrapper.find('[data-testid="folder-sync-safety-confirmation"]').exists()).toBe(true)
175+
expect(wrapper.find('[data-testid="folder-sync-safety-confirmation"]').text()).toContain(
176+
'package/app.exe',
177+
)
178+
expect(wrapper.find('[data-testid="folder-sync-safety-confirmation"]').text()).toContain(
179+
'prod/old.dll',
180+
)
181+
await wrapper.find('[data-testid="folder-sync-confirm-safety"]').trigger('click')
173182
await flushPromises()
174183

175184
expect(executeFolderSync).toHaveBeenCalledWith({
@@ -709,4 +718,60 @@ describe('FolderSyncView', () => {
709718
expect(createFolderEntry).toHaveBeenCalledWith({ path: 'D:/deploy/prod/Fresh' })
710719
expect(previewFolderSync).toHaveBeenCalled()
711720
})
721+
722+
it('cancels overwrite/delete confirmation without executing', async () => {
723+
const wrapper = mount(FolderSyncView, {
724+
global: {
725+
stubs: {
726+
NButton: {
727+
props: ['disabled', 'loading'],
728+
emits: ['click'],
729+
template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>',
730+
},
731+
},
732+
},
733+
})
734+
735+
await wrapper.find('[data-testid="folder-sync-left-path"]').setValue('D:/deploy/package')
736+
await wrapper.find('[data-testid="folder-sync-right-path"]').setValue('D:/deploy/prod')
737+
await wrapper.find('[data-testid="folder-sync-preview"]').trigger('click')
738+
await flushPromises()
739+
await wrapper.find('[data-testid="folder-sync-run"]').trigger('click')
740+
741+
expect(wrapper.find('[data-testid="folder-sync-safety-confirmation"]').exists()).toBe(true)
742+
await wrapper.find('[data-testid="folder-sync-cancel-safety"]').trigger('click')
743+
await flushPromises()
744+
745+
expect(wrapper.find('[data-testid="folder-sync-safety-confirmation"]').exists()).toBe(false)
746+
expect(executeFolderSync).not.toHaveBeenCalled()
747+
})
748+
749+
it('skips the safety panel when overwrite and delete confirms are off', async () => {
750+
const settings = useSettingsStore()
751+
752+
settings.setConfirmBeforeSyncOverwrite(false)
753+
settings.setConfirmBeforeSyncDelete(false)
754+
755+
const wrapper = mount(FolderSyncView, {
756+
global: {
757+
stubs: {
758+
NButton: {
759+
props: ['disabled', 'loading'],
760+
emits: ['click'],
761+
template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>',
762+
},
763+
},
764+
},
765+
})
766+
767+
await wrapper.find('[data-testid="folder-sync-left-path"]').setValue('D:/deploy/package')
768+
await wrapper.find('[data-testid="folder-sync-right-path"]').setValue('D:/deploy/prod')
769+
await wrapper.find('[data-testid="folder-sync-preview"]').trigger('click')
770+
await flushPromises()
771+
await wrapper.find('[data-testid="folder-sync-run"]').trigger('click')
772+
await flushPromises()
773+
774+
expect(wrapper.find('[data-testid="folder-sync-safety-confirmation"]').exists()).toBe(false)
775+
expect(executeFolderSync).toHaveBeenCalled()
776+
})
712777
})

src/views/FolderSyncView.vue

Lines changed: 150 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ const previewRows = ref<SyncPreviewRow[]>([])
171171
const completedOperations = ref(0)
172172
const syncLogs = ref<string[]>([])
173173
const planAccepted = ref(false)
174+
const pendingSyncSafetyRows = ref<SyncPreviewRow[]>([])
174175
const syncChromeMessage = ref('')
175176
const collapsedPrefixes = ref<Set<string>>(new Set())
176177
const showSyncFilters = ref(false)
@@ -738,6 +739,7 @@ async function previewSync(): Promise<void> {
738739
syncLogs.value = []
739740
syncRunError.value = undefined
740741
planAccepted.value = false
742+
pendingSyncSafetyRows.value = []
741743
syncChromeMessage.value = ''
742744
collapsedPrefixes.value = new Set()
743745
checkedRowIds.value = new Set()
@@ -756,6 +758,32 @@ async function previewSync(): Promise<void> {
756758
}
757759
}
758760
761+
function isDeleteOverride(action: FolderSyncOverrideAction): boolean {
762+
return action === 'deleteLeft' || action === 'deleteRight'
763+
}
764+
765+
function isCopyOverride(action: FolderSyncOverrideAction): boolean {
766+
return action === 'copyLeftToRight' || action === 'copyRightToLeft'
767+
}
768+
769+
function collectRiskySyncRows(): SyncPreviewRow[] {
770+
return previewRows.value.filter((row) => {
771+
if (row.overrideAction === 'leave') {
772+
return false
773+
}
774+
775+
if (isDeleteOverride(row.overrideAction) && settings.confirmBeforeSyncDelete) {
776+
return true
777+
}
778+
779+
return isCopyOverride(row.overrideAction) && settings.confirmBeforeSyncOverwrite
780+
})
781+
}
782+
783+
function syncOverrideActionLabel(action: FolderSyncOverrideAction): string {
784+
return t(overrideOptions.find((option) => option.value === action)?.labelKey ?? 'ui.leaveAlone')
785+
}
786+
759787
async function runSync(): Promise<void> {
760788
if (!canRunSync.value) {
761789
return
@@ -765,17 +793,29 @@ async function runSync(): Promise<void> {
765793
acceptSyncPlan()
766794
}
767795
768-
const deleteCount = previewRows.value.filter(
769-
(row) => row.overrideAction === 'deleteLeft' || row.overrideAction === 'deleteRight',
770-
).length
796+
const riskyRows = collectRiskySyncRows()
771797
772-
if (deleteCount > 0 && settings.confirmBeforeSyncDelete) {
773-
// eslint-disable-next-line no-alert -- Options Confirmations sync-delete gate
774-
const accepted = window.confirm(t('ui.confirmBeforeSyncDeleteHint'))
798+
if (riskyRows.length > 0) {
799+
pendingSyncSafetyRows.value = riskyRows
775800
776-
if (!accepted) {
777-
return
778-
}
801+
return
802+
}
803+
804+
await executeSyncNow()
805+
}
806+
807+
function confirmSyncSafety(): void {
808+
pendingSyncSafetyRows.value = []
809+
void executeSyncNow()
810+
}
811+
812+
function cancelSyncSafety(): void {
813+
pendingSyncSafetyRows.value = []
814+
}
815+
816+
async function executeSyncNow(): Promise<void> {
817+
if (!canRunSync.value) {
818+
return
779819
}
780820
781821
syncRunning.value = true
@@ -875,6 +915,7 @@ function acceptSyncPlan(): void {
875915
row.overrideAction = row.plannedAction
876916
}
877917
planAccepted.value = true
918+
pendingSyncSafetyRows.value = []
878919
syncChromeMessage.value = t('status.syncPlanAccepted')
879920
}
880921
@@ -883,11 +924,18 @@ function cancelSyncOverrides(): void {
883924
row.overrideAction = 'leave'
884925
}
885926
planAccepted.value = false
927+
pendingSyncSafetyRows.value = []
886928
syncChromeMessage.value = t('status.syncPlanCancelled')
887929
}
888930
889931
function resetRowOverride(row: SyncPreviewRow): void {
890932
row.overrideAction = row.plannedAction
933+
pendingSyncSafetyRows.value = []
934+
}
935+
936+
function onRowOverrideChange(): void {
937+
planAccepted.value = false
938+
pendingSyncSafetyRows.value = []
891939
}
892940
893941
function syncOverrideTargetRows(): SyncPreviewRow[] {
@@ -912,6 +960,7 @@ function applySyncOverrideAction(action: FolderSyncOverrideAction): void {
912960
for (const row of targets) {
913961
row.overrideAction = action
914962
}
963+
pendingSyncSafetyRows.value = []
915964
916965
const label =
917966
overrideOptions.find((option) => option.value === action)?.labelKey ?? 'ui.leaveAlone'
@@ -1698,6 +1747,45 @@ watch(
16981747
planAccepted ? $t('status.syncPlanAccepted') : $t('status.syncPlanPending')
16991748
}}</em>
17001749
</header>
1750+
<section
1751+
v-if="pendingSyncSafetyRows.length > 0"
1752+
class="sync-safety-confirmation"
1753+
data-testid="folder-sync-safety-confirmation"
1754+
>
1755+
<div>
1756+
<strong>{{ $t('ui.confirmRiskySyncActions') }}</strong>
1757+
<span>{{
1758+
$t('status.overwriteDeleteOperationsNeedReview', {
1759+
count: pendingSyncSafetyRows.length,
1760+
})
1761+
}}</span>
1762+
</div>
1763+
<ul>
1764+
<li
1765+
v-for="row in pendingSyncSafetyRows"
1766+
:key="row.id"
1767+
>
1768+
<strong>{{ syncOverrideActionLabel(row.overrideAction) }}</strong>
1769+
<span>{{ row.targetPath ?? row.relativePath }}</span>
1770+
</li>
1771+
</ul>
1772+
<div class="sync-safety-actions">
1773+
<NButton
1774+
size="small"
1775+
secondary
1776+
data-testid="folder-sync-cancel-safety"
1777+
@click="cancelSyncSafety"
1778+
>{{ $t('ui.cancel') }}</NButton
1779+
>
1780+
<NButton
1781+
size="small"
1782+
type="primary"
1783+
data-testid="folder-sync-confirm-safety"
1784+
@click="confirmSyncSafety"
1785+
>{{ $t('ui.confirmSync') }}</NButton
1786+
>
1787+
</div>
1788+
</section>
17011789
<div class="sync-preview-table">
17021790
<div class="sync-preview-row sync-preview-head">
17031791
<span>{{ $t('ui.select') }}</span>
@@ -1739,7 +1827,7 @@ watch(
17391827
v-model="row.overrideAction"
17401828
:data-testid="`sync-override-${row.id}`"
17411829
@click.stop
1742-
@change="planAccepted = false"
1830+
@change="onRowOverrideChange"
17431831
>
17441832
<option
17451833
v-for="option in overrideOptions"
@@ -2086,6 +2174,58 @@ h1 {
20862174
background: var(--app-surface);
20872175
}
20882176
2177+
.sync-safety-confirmation {
2178+
display: grid;
2179+
grid-template-columns: minmax(180px, 0.8fr) minmax(260px, 1fr) auto;
2180+
align-items: center;
2181+
gap: 6px;
2182+
padding: 2px 4px;
2183+
border: 1px solid var(--diff-deleted-fg);
2184+
border-radius: 0;
2185+
background: var(--app-surface-muted);
2186+
}
2187+
2188+
.sync-safety-confirmation div {
2189+
display: grid;
2190+
gap: 2px;
2191+
}
2192+
2193+
.sync-safety-confirmation strong {
2194+
font-size: 12px;
2195+
}
2196+
2197+
.sync-safety-confirmation span {
2198+
color: var(--app-text-muted);
2199+
font-size: 12px;
2200+
}
2201+
2202+
.sync-safety-confirmation ul {
2203+
display: grid;
2204+
gap: 4px;
2205+
min-width: 0;
2206+
margin: 0;
2207+
padding: 0;
2208+
list-style: none;
2209+
}
2210+
2211+
.sync-safety-confirmation li {
2212+
display: grid;
2213+
grid-template-columns: 76px minmax(0, 1fr);
2214+
gap: 8px;
2215+
min-width: 0;
2216+
}
2217+
2218+
.sync-safety-confirmation li span {
2219+
overflow: hidden;
2220+
text-overflow: ellipsis;
2221+
white-space: nowrap;
2222+
}
2223+
2224+
.sync-safety-actions {
2225+
display: inline-flex;
2226+
gap: 4px;
2227+
}
2228+
20892229
.sync-preview header {
20902230
display: grid;
20912231
gap: 2px;

src/views/sessionLinkage.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ describe('session UI to command linkage', () => {
252252

253253
await wrapper.find('[data-testid="folder-sync-accept"]').trigger('click')
254254
await wrapper.find('[data-testid="folder-sync-run"]').trigger('click')
255+
expect(wrapper.find('[data-testid="folder-sync-safety-confirmation"]').exists()).toBe(true)
256+
await wrapper.find('[data-testid="folder-sync-confirm-safety"]').trigger('click')
255257
await flushPromises()
256258

257259
expectCommand('execute_folder_sync', {

0 commit comments

Comments
 (0)