From bb3635e51762f92aa5408c4e8d4a176b58790682 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Tue, 28 Jul 2026 11:50:45 +0700 Subject: [PATCH] Add a Kerio sample file to the bulk import Only the plain four-column format had a downloadable example, leaving the Kerio route undocumented in the UI. Each import button now carries its own sample link underneath: the plain comma-separated layout and a Kerio export with the Name;FullName;Description;Enable header, a disabled row included to show what the import skips. Co-Authored-By: Claude Opus 5 (1M context) --- web/src/app.css | 15 ++++++++- web/src/pages/TaskDetail.tsx | 63 +++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 19 deletions(-) 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
- - - +
+ + +
+
+ + +