@@ -171,6 +171,7 @@ const previewRows = ref<SyncPreviewRow[]>([])
171171const completedOperations = ref (0 )
172172const syncLogs = ref <string []>([])
173173const planAccepted = ref (false )
174+ const pendingSyncSafetyRows = ref <SyncPreviewRow []>([])
174175const syncChromeMessage = ref (' ' )
175176const collapsedPrefixes = ref <Set <string >>(new Set ())
176177const 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+
759787async 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
889931function 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
893941function 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 ;
0 commit comments