feat(web): per-account folders button + edit-mapping modal

This commit is contained in:
2026-07-03 11:56:47 +07:00
parent 1ba7b54e05
commit 964e4cfc33
2 changed files with 98 additions and 18 deletions
+7
View File
@@ -659,6 +659,13 @@ table.tbl a.rowlink:hover {
text-align: right; text-align: right;
} }
.row-actions {
display: inline-flex;
gap: 12px;
align-items: center;
justify-content: flex-end;
}
.empty-row td { .empty-row td {
text-align: center; text-align: center;
color: var(--fg-faint); color: var(--fg-faint);
+77 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from 'react' 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 { connectTaskWS, type TaskEvent } from '../ws'
import { StatusBadge } from '../components/StatusBadge' import { StatusBadge } from '../components/StatusBadge'
import { useConfirm } from '../components/ConfirmProvider' import { useConfirm } from '../components/ConfirmProvider'
@@ -75,6 +75,13 @@ export function TaskDetail({ id }: { id: number }) {
const [form, setForm] = useState(emptyAccount) const [form, setForm] = useState(emptyAccount)
const [busy, setBusy] = useState<'test' | 'run' | 'add' | 'import' | 'delete' | 'probe' | null>(null) 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 [mapState, setMapState] = useState<{ src: string[]; dst: string[]; creds: typeof emptyAccount } | null>(null)
const [editMap, setEditMap] = useState<{
accId: number
src: string[]
dst: string[]
mapping: Record<string, string>
excluded: string[]
} | null>(null)
const confirm = useConfirm() const confirm = useConfirm()
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [live, setLive] = useState<Record<number, LiveProgress>>({}) const [live, setLive] = useState<Record<number, LiveProgress>>({})
@@ -186,13 +193,13 @@ export function TaskDetail({ id }: { id: number }) {
} }
} }
async function confirmMapping(mapping: Record<string, string>) { async function confirmMapping(mapping: Record<string, string>, excluded: string[]) {
if (!mapState) return if (!mapState) return
setBusy('add') setBusy('add')
setError(null) setError(null)
try { try {
await createAccount(id, mapState.creds) const { id: accId } = await createAccount(id, mapState.creds)
await setFolderMapping(id, mapping) await setAccountFolderMapping(id, accId, mapping, excluded)
setForm(emptyAccount) setForm(emptyAccount)
setMapState(null) setMapState(null)
reload() reload()
@@ -203,6 +210,47 @@ export function TaskDetail({ id }: { id: number }) {
} }
} }
async function onEditFolders(a: { id: number; src_login: string; folder_mapping?: Record<string, string>; 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<string, string>, 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() { function downloadExampleCSV() {
const sample = [ const sample = [
'alice@source.example,SrcPass1,alice@dest.example,DstPass1', 'alice@source.example,SrcPass1,alice@dest.example,DstPass1',
@@ -528,6 +576,17 @@ export function TaskDetail({ id }: { id: number }) {
<td className="num-cell">{live[a.id]?.skipped ?? a.skipped}</td> <td className="num-cell">{live[a.id]?.skipped ?? a.skipped}</td>
<td className="num-cell">{a.errors}</td> <td className="num-cell">{a.errors}</td>
<td className="num-cell"> <td className="num-cell">
<div className="row-actions">
{a.status !== 'running' && (
<button
type="button"
className="link-btn"
onClick={() => onEditFolders(a)}
disabled={busy !== null}
>
folders
</button>
)}
{a.status === 'running' ? ( {a.status === 'running' ? (
<button type="button" className="link-btn danger" onClick={() => onCancelAccount(a.id)}> <button type="button" className="link-btn danger" onClick={() => onCancelAccount(a.id)}>
cancel cancel
@@ -542,6 +601,7 @@ export function TaskDetail({ id }: { id: number }) {
remove remove
</button> </button>
)} )}
</div>
</td> </td>
</tr> </tr>
)) ))
@@ -558,10 +618,23 @@ export function TaskDetail({ id }: { id: number }) {
srcFolders={mapState.src} srcFolders={mapState.src}
dstFolders={mapState.dst} dstFolders={mapState.dst}
initialMapping={task.folder_mapping ?? {}} initialMapping={task.folder_mapping ?? {}}
initialExcluded={[]}
onCancel={() => setMapState(null)} onCancel={() => setMapState(null)}
onConfirm={confirmMapping} onConfirm={confirmMapping}
/> />
)} )}
{editMap && (
<FolderMappingModal
key={`edit-${editMap.accId}`}
open
srcFolders={editMap.src}
dstFolders={editMap.dst}
initialMapping={editMap.mapping}
initialExcluded={editMap.excluded}
onCancel={() => setEditMap(null)}
onConfirm={saveEditMapping}
/>
)}
</> </>
) )
} }