diff --git a/web/src/app.css b/web/src/app.css index 6eaf533..6818ac4 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -905,11 +905,24 @@ table.tbl a.rowlink:hover { .upload-row { display: flex; - align-items: center; + align-items: flex-start; gap: 12px; flex-wrap: wrap; } +/* One import route per column: the action on top, its sample file underneath. */ +.upload-item { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 6px; +} + +.upload-item .link-btn { + align-self: center; + text-align: center; +} + .file-btn { position: relative; overflow: hidden; diff --git a/web/src/pages/TaskDetail.tsx b/web/src/pages/TaskDetail.tsx index ccaed86..1ace45a 100644 --- a/web/src/pages/TaskDetail.tsx +++ b/web/src/pages/TaskDetail.tsx @@ -260,20 +260,40 @@ export function TaskDetail({ id }: { id: number }) { } } - function downloadExampleCSV() { - const sample = [ - 'alice@source.example,SrcPass1,alice@dest.example,DstPass1', - 'bob@source.example,SrcPass2,bob@dest.example,DstPass2', - 'carol@source.example,SrcPass3,carol@dest.example,DstPass3', - ].join('\n') + '\n' - const url = URL.createObjectURL(new Blob([sample], { type: 'text/csv' })) + function downloadCSV(name: string, content: string) { + const url = URL.createObjectURL(new Blob([content], { type: 'text/csv' })) const a = document.createElement('a') a.href = url - a.download = 'imap-copier-accounts-example.csv' + a.download = name a.click() URL.revokeObjectURL(url) } + // Plain import: comma-separated, no header, src_login,src_pass,dst_login,dst_pass. + function downloadExampleCSV() { + const sample = + [ + 'alice@source.example,SrcPass1,alice@dest.example,DstPass1', + 'bob@source.example,SrcPass2,bob@dest.example,DstPass2', + 'carol@source.example,SrcPass3,carol@dest.example,DstPass3', + ].join('\n') + '\n' + downloadCSV('imap-copier-accounts-example.csv', sample) + } + + // Kerio Connect export: semicolon-separated with a Name;FullName;Description;Enable + // header. The password lives in Description; disabled rows and admin are skipped + // on import, and the domain is supplied in the import dialog. + function downloadKerioExampleCSV() { + const sample = + [ + 'Name;FullName;Description;Enable', + 'alice;Alice Smith;SrcPass1;Yes', + 'bob;Bob Jones;SrcPass2;Yes', + 'carol;Carol White (disabled, skipped);SrcPass3;No', + ].join('\n') + '\n' + downloadCSV('kerio-users-example.csv', sample) + } + async function onFileChosen(e: ChangeEvent) { const file = e.target.files?.[0] if (!file) return @@ -556,16 +576,23 @@ export function TaskDetail({ id }: { id: number }) {
or bulk import
- - - +
+ + +
+
+ + +