feat(web): per-account folders button + edit-mapping modal
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<string, string>
|
||||
excluded: string[]
|
||||
} | null>(null)
|
||||
const confirm = useConfirm()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
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
|
||||
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<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() {
|
||||
const sample = [
|
||||
'alice@source.example,SrcPass1,alice@dest.example,DstPass1',
|
||||
@@ -528,20 +576,32 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
<td className="num-cell">{live[a.id]?.skipped ?? a.skipped}</td>
|
||||
<td className="num-cell">{a.errors}</td>
|
||||
<td className="num-cell">
|
||||
{a.status === 'running' ? (
|
||||
<button type="button" className="link-btn danger" onClick={() => onCancelAccount(a.id)}>
|
||||
cancel
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn danger"
|
||||
onClick={() => onDeleteAccount(a.id, a.src_login)}
|
||||
disabled={busy !== null || data?.task.status === 'running'}
|
||||
>
|
||||
remove
|
||||
</button>
|
||||
)}
|
||||
<div className="row-actions">
|
||||
{a.status !== 'running' && (
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
onClick={() => onEditFolders(a)}
|
||||
disabled={busy !== null}
|
||||
>
|
||||
folders
|
||||
</button>
|
||||
)}
|
||||
{a.status === 'running' ? (
|
||||
<button type="button" className="link-btn danger" onClick={() => onCancelAccount(a.id)}>
|
||||
cancel
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn danger"
|
||||
onClick={() => onDeleteAccount(a.id, a.src_login)}
|
||||
disabled={busy !== null || data?.task.status === 'running'}
|
||||
>
|
||||
remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
@@ -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 && (
|
||||
<FolderMappingModal
|
||||
key={`edit-${editMap.accId}`}
|
||||
open
|
||||
srcFolders={editMap.src}
|
||||
dstFolders={editMap.dst}
|
||||
initialMapping={editMap.mapping}
|
||||
initialExcluded={editMap.excluded}
|
||||
onCancel={() => setEditMap(null)}
|
||||
onConfirm={saveEditMapping}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user