Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/lang/en/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
"retry_selected": "Retry Selected",
"cancel_selected": "Cancel Selected",
"delete_selected": "Delete Selected",
"delete_by_path": "Delete by Path",
"cancel_by_path": "Cancel by Path",
"retry_by_path": "Retry by Path",
"path_tips": "Matches tasks whose source or destination path is under this prefix. Does not require selecting tasks from the list.",
"delete_by_path_confirm": "Delete all matching tasks under {{path}}? Running tasks will be canceled first. This cannot be undone.",
"path_explicit_root_required": "Enter / explicitly to operate on all tasks. Inputs such as . or // are not accepted as the root path.",
"path_batch_result": "Matched {{matched}} task(s); processed {{processed}}.",
"filter": "Filter",
"expand": "Expand",
"fold": "Fold",
Expand Down
98 changes: 96 additions & 2 deletions src/pages/manage/tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,31 @@ import {
onCleanup,
Show,
} from "solid-js"
import { Paginator } from "~/components"
import { ModalInput, Paginator } from "~/components"
import { useFetch, useT } from "~/hooks"
import { PEmptyResp, PResp, TaskInfo } from "~/types"
import { PEmptyResp, PResp, Resp, TaskInfo, TaskPathResult } from "~/types"
import { handleResp, notify, r } from "~/utils"
import { TaskCol, cols, Task, TaskOrderBy, TaskLocal } from "./Task"
import { me } from "~/store"

type PathBatchAction = "delete" | "cancel" | "retry"

const normalizesToRoot = (path: string) => {
const normalized = path
.replaceAll("\\", "/")
.split("/")
.reduce<string[]>((parts, part) => {
if (!part || part === ".") return parts
if (part === "..") {
parts.pop()
return parts
}
parts.push(part)
return parts
}, [])
return normalized.length === 0
}

export interface TaskNameAnalyzer {
regex: RegExp
title: (matches: RegExpMatchArray) => string
Expand Down Expand Up @@ -212,6 +230,49 @@ export const Tasks = (props: TasksProps) => {
(): PEmptyResp =>
r.post(`/task/${props.type}/${operateName}_some`, getSelectedId()),
)
const [pathModalOpen, setPathModalOpen] = createSignal(false)
const [pathAction, setPathAction] = createSignal<PathBatchAction>("delete")
const [pathBatchLoading, setPathBatchLoading] = createSignal(false)
const openPathModal = (action: PathBatchAction) => {
setPathAction(action)
setPathModalOpen(true)
}
const submitPathBatch = async (path: string) => {
const trimmed = path.trim()
if (!trimmed) {
notify.warning(t("global.empty_input"))
return
}
if (me().role === 2 && trimmed !== "/" && normalizesToRoot(trimmed)) {
notify.warning(t("tasks.path_explicit_root_required"))
return
}
if (pathAction() === "delete") {
const ok = window.confirm(
t("tasks.delete_by_path_confirm", { path: trimmed }),
)
if (!ok) return
}
setPathBatchLoading(true)
try {
const resp = (await r.post(
`/task/${props.type}/${pathAction()}_by_path`,
{ path: trimmed },
)) as Resp<TaskPathResult>
handleResp(resp, (data) => {
notify.success(
t("tasks.path_batch_result", {
matched: data?.matched ?? 0,
processed: data?.processed ?? 0,
}),
)
setPathModalOpen(false)
refresh()
})
} finally {
setPathBatchLoading(false)
}
}
const notifyIndividualError = (msg: Record<string, string>) => {
Object.entries(msg).forEach(([key, value]) => {
notify.error(`${key}: ${value}`)
Expand Down Expand Up @@ -326,6 +387,31 @@ export const Tasks = (props: TasksProps) => {
>
{t(`tasks.${operateName}_selected`)}
</Button>
<Show when={props.done === "undone"}>
<Button
colorScheme="warning"
variant="outline"
onClick={() => openPathModal("cancel")}
>
{t("tasks.cancel_by_path")}
</Button>
</Show>
<Button
colorScheme="danger"
variant="outline"
onClick={() => openPathModal("delete")}
>
{t("tasks.delete_by_path")}
</Button>
<Show when={props.done === "done" && props.canRetry}>
<Button
colorScheme="primary"
variant="outline"
onClick={() => openPathModal("retry")}
>
{t("tasks.retry_by_path")}
</Button>
</Show>
<Input
width="auto"
placeholder={t(`tasks.filter`)}
Expand All @@ -342,6 +428,14 @@ export const Tasks = (props: TasksProps) => {
</Checkbox>
</Show>
</HStack>
<ModalInput
title={`tasks.${pathAction()}_by_path`}
tips={t("tasks.path_tips")}
opened={pathModalOpen()}
onClose={() => setPathModalOpen(false)}
loading={pathBatchLoading()}
onSubmit={submitPathBatch}
/>
<VStack
w={{ "@initial": "1024px", "@lg": "$full" }}
overflowX="auto"
Expand Down
5 changes: 5 additions & 0 deletions src/types/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export interface TaskInfo {
total_bytes: number
error: string
}

export interface TaskPathResult {
matched: number
processed: number
}