From 782e6076a78bf2c8e1281a23bfc31317e2a3ee9c Mon Sep 17 00:00:00 2001 From: toorux Date: Fri, 9 Oct 2026 19:18:08 +0800 Subject: [PATCH] Redesign upload confirmation with visible conflict choices --- tests/file_upload_fixture.tsx | 4 +++- ui/locales/en-US.json | 9 ++++++++- ui/locales/zh-CN.json | 9 ++++++++- ui/src/file-manager.css | 27 +++++++++++++++++++++++++++ ui/src/file-manager.tsx | 19 +++++++++++++++---- 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/tests/file_upload_fixture.tsx b/tests/file_upload_fixture.tsx index cad4df7..7859581 100644 --- a/tests/file_upload_fixture.tsx +++ b/tests/file_upload_fixture.tsx @@ -31,8 +31,10 @@ const modal=()=>document.querySelector('.modal');const click=(label:string,paren const folder=document.querySelector('.file-table [data-file-drop-directory]')!;drop(folder,true);await until(modal); if(modal()!.querySelector('code')?.textContent!==dest)throw Error('Folder target mismatch'); if(calls.some(p=>p.operation==='upload.start')&&lang==='en-US')throw Error('Upload before confirmation'); + if(modal()!.querySelectorAll('input[type="radio"]').length!==3||modal()!.querySelector('select'))throw Error('Conflict choices must be visible'); + const choices=modal()!.querySelectorAll('input[type="radio"]');if(!choices[0].checked)throw Error('Keep both must be default');choices[2].click();await wait();if(!modal()!.querySelector('[role="status"]'))throw Error('Missing overwrite warning');choices[0].click();await wait(); await wait(1400);console.log('FRAMELY_PREVIEW_INSTALL_UPLOAD_CONFIRM_'+(lang==='en-US'?'EN':'ZH'));await wait(200); - click(lang==='en-US'?'Confirm':'确认',modal()!);await until(()=>!modal()); + click(lang==='en-US'?'Start upload':'开始上传',modal()!);await until(()=>!modal()); if(document.querySelector('.file-upload-popover'))throw Error('Queue opened without a click'); document.querySelector('.file-upload-toggle')!.click();await until(()=>document.querySelector('.file-upload-task.uploading')); if(!document.querySelector('.file-upload-batch-count')?.textContent?.includes('2'))throw Error('Missing total file count'); diff --git a/ui/locales/en-US.json b/ui/locales/en-US.json index c0cd1c3..20ef4d9 100644 --- a/ui/locales/en-US.json +++ b/ui/locales/en-US.json @@ -1047,6 +1047,13 @@ "Upload destination moved or changed": "Upload destination moved or changed", "Upload file conflicts with a directory or special file": "Upload file conflicts with a directory or special file", "Upload directory conflicts with a file or symbolic link": "Upload directory conflicts with a file or symbolic link", - "{0} 项进行中 · 共 {1} 项": "{0} active · {1} total" + "{0} 项进行中 · 共 {1} 项": "{0} active · {1} total", + "上传到": "Upload to", + "{0} 个文件": "{0} files", + "{0} 个文件夹": "{0} folders", + "自动重命名上传的文件,保留原文件。": "Rename incoming files and keep the originals.", + "不上传同名文件,保留原文件。": "Leave matching files unchanged without uploading them.", + "用上传的文件替换原文件。": "Replace existing files with the incoming files.", + "开始上传": "Start upload" } } diff --git a/ui/locales/zh-CN.json b/ui/locales/zh-CN.json index 64e694e..bbcd8c6 100644 --- a/ui/locales/zh-CN.json +++ b/ui/locales/zh-CN.json @@ -1047,6 +1047,13 @@ "Upload destination moved or changed": "上传目标已被移动或替换。", "Upload file conflicts with a directory or special file": "上传文件与目录或特殊文件同名。", "Upload directory conflicts with a file or symbolic link": "上传文件夹与文件或符号链接同名。", - "{0} 项进行中 · 共 {1} 项": "{0} 项进行中 · 共 {1} 项" + "{0} 项进行中 · 共 {1} 项": "{0} 项进行中 · 共 {1} 项", + "上传到": "上传到", + "{0} 个文件": "{0} 个文件", + "{0} 个文件夹": "{0} 个文件夹", + "自动重命名上传的文件,保留原文件。": "自动重命名上传的文件,保留原文件。", + "不上传同名文件,保留原文件。": "不上传同名文件,保留原文件。", + "用上传的文件替换原文件。": "用上传的文件替换原文件。", + "开始上传": "开始上传" } } diff --git a/ui/src/file-manager.css b/ui/src/file-manager.css index 07040ff..abc626e 100644 --- a/ui/src/file-manager.css +++ b/ui/src/file-manager.css @@ -305,3 +305,30 @@ @media(max-width:767px) and (prefers-reduced-motion:no-preference){.manager .file-manager .file-sidebar{transition:transform .18s ease-out,visibility .18s}} .file-mobile-select-label{display:none} @media(max-width:767px){.manager .file-table th.file-check{position:relative;height:44px}.manager .file-page-check{display:flex;align-items:center;margin:0}.manager .file-select-mode .file-mobile-select-label{display:block;position:absolute;left:42px;top:50%;transform:translateY(-50%);white-space:nowrap;font-size:13px;color:var(--file-muted)}} + +/* Upload review: destination first, then a single mutually exclusive choice. */ +.file-manager .modal.file-upload-confirm{width:min(540px,calc(100% - 32px));left:50%;right:auto;transform:translate(-50%,-50%)} +.file-upload-confirm h2{margin-bottom:24px} +.file-upload-destination{display:flex;align-items:flex-start;gap:14px} +.file-upload-destination>svg{flex:none;margin-top:4px;color:#c6c6ca} +.file-upload-destination>div{min-width:0} +.file-upload-destination span{display:block;font-size:14px;color:#bcbcc2;margin-bottom:6px} +.file-upload-review .file-upload-destination code{display:block;padding:0;background:none;border-radius:0;font-size:18px;line-height:1.5;color:#f1f1f3;white-space:normal;overflow-wrap:anywhere} +.file-upload-summary{display:flex;flex-wrap:wrap;gap:8px 18px;margin:18px 0 26px;padding-bottom:22px;border-bottom:1px solid #ffffff16;font-size:14px;color:#c6c6cc} +.file-upload-summary strong{margin-left:auto;font-weight:500;color:#ededf0} +.file-upload-policies{border:0;padding:0;margin:0;min-width:0} +.file-upload-policies legend{padding:0;margin-bottom:12px;font-size:15px;font-weight:600;color:#ededf0} +.file-manager .modal .file-upload-policies label{display:flex;flex-direction:row;align-items:center;gap:14px;padding:14px 16px;margin:0 0 8px;border:1px solid #ffffff18;border-radius:10px;background:#ffffff03;cursor:pointer} +.file-upload-policies label:hover{background:#ffffff08} +.file-upload-policies label.selected{border-color:#ffffff50;background:#ffffff0d;box-shadow:inset 0 1px 0 #ffffff09} +.file-manager .modal .file-upload-policies input[type=radio]{appearance:auto;flex:none;width:18px;height:18px;min-height:0;margin:0;padding:0;accent-color:#d9d9de} +.file-upload-policies label:focus-within{outline:2px solid #b9d6e9;outline-offset:2px} +.file-upload-policies strong{display:block;font-size:15px;font-weight:600} +.file-upload-policies small{display:block;font-size:13px;line-height:1.5;color:#bcbcc2;margin-top:4px} +.file-upload-review .file-upload-note{font-size:13px;line-height:1.6;color:#bcbcc2;margin:14px 0 0} +.file-upload-review .file-upload-warning{font-size:13px;line-height:1.6;color:#eed4ab;margin:12px 0 0} +.file-upload-confirm footer{margin-top:24px} +.file-upload-confirm footer button{white-space:nowrap} +@media(max-width:767px){.file-upload-confirm h2{margin-bottom:20px}.file-upload-review .file-upload-destination code{font-size:16px}.file-upload-policies label{min-height:70px}.file-upload-summary{margin-bottom:20px}} + +.file-manager:has(.modal) .file-upload-queue{visibility:hidden} diff --git a/ui/src/file-manager.tsx b/ui/src/file-manager.tsx index 0e1c923..40fff58 100644 --- a/ui/src/file-manager.tsx +++ b/ui/src/file-manager.tsx @@ -200,11 +200,22 @@ export function FileManager({active=true}:{active?:boolean}){ {active&&menu&&
event.preventDefault()} onClick={event=>{if((event.target as HTMLElement).closest('button:not(:disabled)'))setMenu(null);}} onKeyDown={event=>{const buttons=Array.from(event.currentTarget.querySelectorAll('button:not(:disabled)'));if(['ArrowDown','ArrowUp','Home','End'].includes(event.key)){event.preventDefault();const index=buttons.indexOf(document.activeElement as HTMLButtonElement),next=event.key==='Home'?0:event.key==='End'?buttons.length-1:(index+(event.key==='ArrowDown'?1:-1)+buttons.length)%buttons.length;buttons[next]?.focus();}if(event.key==='Escape')setMenu(null);}}> {menu.blank?<>
:<>{actions}
{picked?.directory&&selectedCount===1&&}}
} - {active&&dialog&&

{dialog.title}

{error&&

{error}

}{dialog.kind==='properties'&&dialog.entry&&

{dialog.entry.path}

{bytes(dialog.entry.size)} · UID {dialog.entry.uid} / GID {dialog.entry.gid}

{dialog.entry.linkTarget&&

{dialog.entry.linkTarget}

}
}{['create','folder','rename','bookmark','bookmark.rename','compress','extract','properties'].includes(dialog.kind)&&} - {dialog.kind==='upload'&&

{t('上传目标')}

{dialog.path}

{t('{0} 个文件 · {1} 个文件夹 · {2}',{0:dialog.entries?.filter(e=>!e.directory).length??0,1:dialog.entries?.filter(e=>e.directory).length??0,2:bytes(dialog.entries?.reduce((n,e)=>n+(e.file?.size??0),0)??0)})}

{t('同名文件夹合并,现有其他文件保留。')}

{dialog.entries?.some(e=>e.error)&&

{t('部分路径存在冲突或不可访问,这些任务会单独显示失败。')}

}
} + {active&&dialog&&

{dialog.title}

{error&&

{error}

}{dialog.kind==='properties'&&dialog.entry&&

{dialog.entry.path}

{bytes(dialog.entry.size)} · UID {dialog.entry.uid} / GID {dialog.entry.gid}

{dialog.entry.linkTarget&&

{dialog.entry.linkTarget}

}
}{['create','folder','rename','bookmark','bookmark.rename','compress','extract','properties'].includes(dialog.kind)&&} + {dialog.kind==='upload'&&
+
{t('上传到')}{dialog.path}
+
{t('{0} 个文件',{0:dialog.entries?.filter(e=>!e.directory).length??0})}{t('{0} 个文件夹',{0:dialog.entries?.filter(e=>e.directory).length??0})}{bytes(dialog.entries?.reduce((n,e)=>n+(e.file?.size??0),0)??0)}
+
{t('遇到同名文件时')}{[ + {id:'keep',name:'保留两份',description:'自动重命名上传的文件,保留原文件。'}, + {id:'skip',name:'跳过',description:'不上传同名文件,保留原文件。'}, + {id:'overwrite',name:'覆盖',description:'用上传的文件替换原文件。'} + ].map(option=>)}
+

{t('同名文件夹合并,现有其他文件保留。')}

+ {policy==='overwrite'&&

{t('确认后会替换同名文件,不删除目录中的其他文件。')}

} + {dialog.entries?.some(e=>e.error)&&

{t('部分路径存在冲突或不可访问,这些任务会单独显示失败。')}

} +
} {dialog.kind==='delete'&&<>

{t('请选择删除方式。')}

{policy==='overwrite'&&

{t(dialog.kind==='upload'?'确认后会替换同名文件,不删除目录中的其他文件。':'确认后会覆盖同名文件或整个同名文件夹。')}

}}{dialog.kind==='compress'&&

{t('支持 .zip、.tar 和 .tar.gz。')}

}{dialog.kind==='trash.purge'&&

{t('永久删除无法撤销。')}

}{dialog.kind==='editor.discard'&&

{t('关闭将丢弃未保存的修改。')}

}{dialog.kind==='editor.overwrite'&&

{t('文件已被外部修改。可重新加载,或确认覆盖。')}

} -
{dialog.kind==='editor.overwrite'&&}
} + {['paste','compress','extract','trash.restore'].includes(dialog.kind)&&<>

{t('遇到同名文件时')}