Add a defaults button to the folder mapping dialog
Mapping the Exchange/Kerio special folders onto mailcow's by hand is repetitive work that scales with the number of accounts. "By default" maps Deleted Items to Trash, Junk E-mail to Junk, Sent Items to Sent and unchecks Public Folders, which mailcow has no counterpart for. The destination select only offered the source folder as a name to create, so a target missing on the destination could not be selected at all. It now also offers the current selection, and marks any name absent from the destination as "(create)". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-1
@@ -488,11 +488,23 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-toolbar .btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.map-all {
|
.map-all {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
|
|||||||
@@ -20,6 +20,21 @@ function defaultDst(src: string, dstFolders: string[], initial: Record<string, s
|
|||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exchange/Kerio special folders and their mailcow counterparts. Keyed by the
|
||||||
|
// lowercased source name so casing differences between servers don't matter.
|
||||||
|
const DEFAULT_TARGETS: Record<string, string> = {
|
||||||
|
'deleted items': 'Trash',
|
||||||
|
'deleted messages': 'Trash',
|
||||||
|
'junk e-mail': 'Junk',
|
||||||
|
'junk email': 'Junk',
|
||||||
|
spam: 'Junk',
|
||||||
|
'sent items': 'Sent',
|
||||||
|
'sent messages': 'Sent',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source folders with no counterpart on mailcow — unchecked by the defaults.
|
||||||
|
const DEFAULT_EXCLUDED = new Set(['public folders'])
|
||||||
|
|
||||||
export function FolderMappingModal({
|
export function FolderMappingModal({
|
||||||
open, srcFolders, dstFolders, initialMapping, initialExcluded, accountLabel, onConfirm, onCancel,
|
open, srcFolders, dstFolders, initialMapping, initialExcluded, accountLabel, onConfirm, onCancel,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@@ -31,16 +46,40 @@ export function FolderMappingModal({
|
|||||||
|
|
||||||
// Options per select: all destination folders, plus the source name itself
|
// Options per select: all destination folders, plus the source name itself
|
||||||
// (marked "create") when it does not already exist on the destination.
|
// (marked "create") when it does not already exist on the destination.
|
||||||
|
const valueFor = (src: string) => choice[src] ?? defaultDst(src, dstFolders, initialMapping)
|
||||||
|
|
||||||
|
// Options per select: all destination folders, plus any name not present there
|
||||||
|
// — the source folder itself and the current selection — marked "create".
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const set = new Set(dstFolders)
|
const set = new Set(dstFolders)
|
||||||
return (src: string) => {
|
return (src: string, current: string) => {
|
||||||
const opts = [...dstFolders]
|
const opts = [...dstFolders]
|
||||||
if (!set.has(src)) opts.unshift(src)
|
if (!set.has(current)) opts.unshift(current)
|
||||||
|
if (!set.has(src) && src !== current) opts.unshift(src)
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
}, [dstFolders])
|
}, [dstFolders])
|
||||||
|
|
||||||
const valueFor = (src: string) => choice[src] ?? defaultDst(src, dstFolders, initialMapping)
|
// Collapse the Exchange/Kerio folder layout onto mailcow's in one click: the
|
||||||
|
// per-account mapping is otherwise repetitive work when importing many users.
|
||||||
|
function applyDefaults() {
|
||||||
|
const nextChoice = { ...choice }
|
||||||
|
const nextSynced = { ...synced }
|
||||||
|
for (const src of srcFolders) {
|
||||||
|
const key = src.trim().toLowerCase()
|
||||||
|
if (DEFAULT_EXCLUDED.has(key)) {
|
||||||
|
nextSynced[src] = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const target = DEFAULT_TARGETS[key]
|
||||||
|
if (target) {
|
||||||
|
nextChoice[src] = target
|
||||||
|
nextSynced[src] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setChoice(nextChoice)
|
||||||
|
setSynced(nextSynced)
|
||||||
|
}
|
||||||
|
|
||||||
function confirm() {
|
function confirm() {
|
||||||
const mapping: Record<string, string> = {}
|
const mapping: Record<string, string> = {}
|
||||||
@@ -70,17 +109,27 @@ export function FolderMappingModal({
|
|||||||
Route each source folder to an existing destination folder. Leaving a folder mapped to its own name
|
Route each source folder to an existing destination folder. Leaving a folder mapped to its own name
|
||||||
creates it on the destination if missing (e.g. map <code>Спам</code> → <code>Spam</code> to avoid duplicates).
|
creates it on the destination if missing (e.g. map <code>Спам</code> → <code>Spam</code> to avoid duplicates).
|
||||||
</p>
|
</p>
|
||||||
<label className="map-all">
|
<div className="map-toolbar">
|
||||||
<input
|
<button
|
||||||
type="checkbox"
|
type="button"
|
||||||
checked={srcFolders.every((f) => synced[f] !== false)}
|
className="btn btn-ghost"
|
||||||
onChange={(e) => {
|
onClick={applyDefaults}
|
||||||
const on = e.target.checked
|
title="Map Deleted Items → Trash, Junk E-mail → Junk, Sent Items → Sent and skip Public Folders"
|
||||||
setSynced(Object.fromEntries(srcFolders.map((f) => [f, on])))
|
>
|
||||||
}}
|
By default
|
||||||
/>
|
</button>
|
||||||
sync all folders
|
<label className="map-all">
|
||||||
</label>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={srcFolders.every((f) => synced[f] !== false)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const on = e.target.checked
|
||||||
|
setSynced(Object.fromEntries(srcFolders.map((f) => [f, on])))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
sync all folders
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div className="map-grid">
|
<div className="map-grid">
|
||||||
{srcFolders.map((src) => {
|
{srcFolders.map((src) => {
|
||||||
const on = synced[src] !== false
|
const on = synced[src] !== false
|
||||||
@@ -107,10 +156,10 @@ export function FolderMappingModal({
|
|||||||
disabled={!on}
|
disabled={!on}
|
||||||
onChange={(e) => setChoice((c) => ({ ...c, [src]: e.target.value }))}
|
onChange={(e) => setChoice((c) => ({ ...c, [src]: e.target.value }))}
|
||||||
>
|
>
|
||||||
{options(src).map((f) => (
|
{options(src, val).map((f) => (
|
||||||
<option key={f} value={f}>
|
<option key={f} value={f}>
|
||||||
{f}
|
{f}
|
||||||
{f === src && !dstFolders.includes(src) ? ' (create)' : ''}
|
{dstFolders.includes(f) ? '' : ' (create)'}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
Reference in New Issue
Block a user