feat(web): folder-sync checkboxes + per-account mapping API client
This commit is contained in:
@@ -6,7 +6,8 @@ type Props = {
|
||||
srcFolders: string[]
|
||||
dstFolders: string[]
|
||||
initialMapping: Record<string, string>
|
||||
onConfirm: (mapping: Record<string, string>) => void
|
||||
initialExcluded: string[]
|
||||
onConfirm: (mapping: Record<string, string>, excluded: string[]) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
@@ -18,8 +19,14 @@ function defaultDst(src: string, dstFolders: string[], initial: Record<string, s
|
||||
return src
|
||||
}
|
||||
|
||||
export function FolderMappingModal({ open, srcFolders, dstFolders, initialMapping, onConfirm, onCancel }: Props) {
|
||||
export function FolderMappingModal({
|
||||
open, srcFolders, dstFolders, initialMapping, initialExcluded, onConfirm, onCancel,
|
||||
}: Props) {
|
||||
const [choice, setChoice] = useState<Record<string, string>>({})
|
||||
const [synced, setSynced] = useState<Record<string, boolean>>(() => {
|
||||
const excl = new Set(initialExcluded)
|
||||
return Object.fromEntries(srcFolders.map((f) => [f, !excl.has(f)]))
|
||||
})
|
||||
|
||||
// Options per select: all destination folders, plus the source name itself
|
||||
// (marked "create") when it does not already exist on the destination.
|
||||
@@ -36,12 +43,18 @@ export function FolderMappingModal({ open, srcFolders, dstFolders, initialMappin
|
||||
|
||||
function confirm() {
|
||||
const mapping: Record<string, string> = { ...initialMapping }
|
||||
const excluded: string[] = []
|
||||
for (const src of srcFolders) {
|
||||
if (synced[src] === false) {
|
||||
excluded.push(src)
|
||||
delete mapping[src]
|
||||
continue
|
||||
}
|
||||
const dst = valueFor(src)
|
||||
if (dst === src) delete mapping[src]
|
||||
else mapping[src] = dst
|
||||
}
|
||||
onConfirm(mapping)
|
||||
onConfirm(mapping, excluded)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -51,12 +64,32 @@ export function FolderMappingModal({ open, srcFolders, dstFolders, initialMappin
|
||||
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).
|
||||
</p>
|
||||
<label className="map-all">
|
||||
<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 className="map-grid">
|
||||
{srcFolders.map((src) => {
|
||||
const on = synced[src] !== false
|
||||
const val = valueFor(src)
|
||||
const creates = !dstFolders.includes(val)
|
||||
return (
|
||||
<div className="map-row" key={src}>
|
||||
<div className={`map-row${on ? '' : ' map-row-off'}`} key={src}>
|
||||
<label className="map-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={on}
|
||||
aria-label={`Sync folder ${src}`}
|
||||
onChange={(e) => setSynced((s) => ({ ...s, [src]: e.target.checked }))}
|
||||
/>
|
||||
</label>
|
||||
<span className="map-src" title={src}>
|
||||
{src}
|
||||
</span>
|
||||
@@ -65,6 +98,7 @@ export function FolderMappingModal({ open, srcFolders, dstFolders, initialMappin
|
||||
className="map-select"
|
||||
aria-label={`Destination folder for ${src}`}
|
||||
value={val}
|
||||
disabled={!on}
|
||||
onChange={(e) => setChoice((c) => ({ ...c, [src]: e.target.value }))}
|
||||
>
|
||||
{options(src).map((f) => (
|
||||
@@ -74,7 +108,11 @@ export function FolderMappingModal({ open, srcFolders, dstFolders, initialMappin
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{creates && <span className="map-new">new</span>}
|
||||
{on ? (
|
||||
creates && <span className="map-new">new</span>
|
||||
) : (
|
||||
<span className="map-excluded">excluded</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user