From 964e4cfc3344b7d17ff71bc6c4d6588143230719 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Fri, 3 Jul 2026 11:56:47 +0700 Subject: [PATCH] feat(web): per-account folders button + edit-mapping modal --- web/src/app.css | 7 +++ web/src/pages/TaskDetail.tsx | 109 +++++++++++++++++++++++++++++------ 2 files changed, 98 insertions(+), 18 deletions(-) diff --git a/web/src/app.css b/web/src/app.css index 25bac4e..a03fdc1 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -659,6 +659,13 @@ table.tbl a.rowlink:hover { text-align: right; } +.row-actions { + display: inline-flex; + gap: 12px; + align-items: center; + justify-content: flex-end; +} + .empty-row td { text-align: center; color: var(--fg-faint); diff --git a/web/src/pages/TaskDetail.tsx b/web/src/pages/TaskDetail.tsx index d393809..1689536 100644 --- a/web/src/pages/TaskDetail.tsx +++ b/web/src/pages/TaskDetail.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from 'react' -import { cancelAccount, createAccount, deleteAccount, getTask, importCSV, probeFolders, runTask, setFolderMapping, testAccounts, type TaskDetail as TaskDetailData } from '../api' +import { cancelAccount, createAccount, deleteAccount, getTask, importCSV, probeAccountFolders, probeFolders, runTask, setAccountFolderMapping, testAccounts, type TaskDetail as TaskDetailData } from '../api' import { connectTaskWS, type TaskEvent } from '../ws' import { StatusBadge } from '../components/StatusBadge' import { useConfirm } from '../components/ConfirmProvider' @@ -75,6 +75,13 @@ export function TaskDetail({ id }: { id: number }) { const [form, setForm] = useState(emptyAccount) const [busy, setBusy] = useState<'test' | 'run' | 'add' | 'import' | 'delete' | 'probe' | null>(null) const [mapState, setMapState] = useState<{ src: string[]; dst: string[]; creds: typeof emptyAccount } | null>(null) + const [editMap, setEditMap] = useState<{ + accId: number + src: string[] + dst: string[] + mapping: Record + excluded: string[] + } | null>(null) const confirm = useConfirm() const [error, setError] = useState(null) const [live, setLive] = useState>({}) @@ -186,13 +193,13 @@ export function TaskDetail({ id }: { id: number }) { } } - async function confirmMapping(mapping: Record) { + async function confirmMapping(mapping: Record, excluded: string[]) { if (!mapState) return setBusy('add') setError(null) try { - await createAccount(id, mapState.creds) - await setFolderMapping(id, mapping) + const { id: accId } = await createAccount(id, mapState.creds) + await setAccountFolderMapping(id, accId, mapping, excluded) setForm(emptyAccount) setMapState(null) reload() @@ -203,6 +210,47 @@ export function TaskDetail({ id }: { id: number }) { } } + async function onEditFolders(a: { id: number; src_login: string; folder_mapping?: Record; excluded_folders?: string[] }) { + setBusy('probe') + setError(null) + try { + const res = await probeAccountFolders(id, a.id) + if (!res.src.ok || !res.dst.ok) { + const parts: string[] = [] + if (!res.src.ok) parts.push(`source: ${res.src.error ?? 'login failed'}`) + if (!res.dst.ok) parts.push(`destination: ${res.dst.error ?? 'login failed'}`) + setError(`Folder probe failed — ${parts.join('; ')}`) + return + } + setEditMap({ + accId: a.id, + src: res.src.folders ?? [], + dst: res.dst.folders ?? [], + mapping: a.folder_mapping ?? {}, + excluded: a.excluded_folders ?? [], + }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to probe folders') + } finally { + setBusy(null) + } + } + + async function saveEditMapping(mapping: Record, excluded: string[]) { + if (!editMap) return + setBusy('add') + setError(null) + try { + await setAccountFolderMapping(id, editMap.accId, mapping, excluded) + setEditMap(null) + reload() + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save mapping') + } finally { + setBusy(null) + } + } + function downloadExampleCSV() { const sample = [ 'alice@source.example,SrcPass1,alice@dest.example,DstPass1', @@ -528,20 +576,32 @@ export function TaskDetail({ id }: { id: number }) { {live[a.id]?.skipped ?? a.skipped} {a.errors} - {a.status === 'running' ? ( - - ) : ( - - )} +
+ {a.status !== 'running' && ( + + )} + {a.status === 'running' ? ( + + ) : ( + + )} +
)) @@ -558,10 +618,23 @@ export function TaskDetail({ id }: { id: number }) { srcFolders={mapState.src} dstFolders={mapState.dst} initialMapping={task.folder_mapping ?? {}} + initialExcluded={[]} onCancel={() => setMapState(null)} onConfirm={confirmMapping} /> )} + {editMap && ( + setEditMap(null)} + onConfirm={saveEditMapping} + /> + )} ) }