From bceaeb6e49ba12a8c5a97311ec7bc0eb9148dc0d Mon Sep 17 00:00:00 2001 From: ifloppy <68799904+ifloppy@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:59:46 +0800 Subject: [PATCH 1/2] feat(tasks): add path-prefix batch cancel, delete and retry - Add toolbar actions to cancel, delete or retry tasks by path prefix - Prompt for the path in a modal and confirm before deleting - Report the number of affected tasks and refresh the list - Add `TaskPathResult` type and related translation strings Signed-off-by: ifloppy <68799904+ifloppy@users.noreply.github.com> --- src/lang/en/tasks.json | 6 +++ src/pages/manage/tasks/Tasks.tsx | 77 +++++++++++++++++++++++++++++++- src/types/task.ts | 4 ++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/lang/en/tasks.json b/src/lang/en/tasks.json index a96897f90..62949445e 100644 --- a/src/lang/en/tasks.json +++ b/src/lang/en/tasks.json @@ -26,6 +26,12 @@ "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_batch_result": "{{count}} task(s) affected", "filter": "Filter", "expand": "Expand", "fold": "Fold", diff --git a/src/pages/manage/tasks/Tasks.tsx b/src/pages/manage/tasks/Tasks.tsx index 4bd09de88..7bd8c51e1 100644 --- a/src/pages/manage/tasks/Tasks.tsx +++ b/src/pages/manage/tasks/Tasks.tsx @@ -19,13 +19,15 @@ 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" + export interface TaskNameAnalyzer { regex: RegExp title: (matches: RegExpMatchArray) => string @@ -212,6 +214,44 @@ export const Tasks = (props: TasksProps) => { (): PEmptyResp => r.post(`/task/${props.type}/${operateName}_some`, getSelectedId()), ) + const [pathModalOpen, setPathModalOpen] = createSignal(false) + const [pathAction, setPathAction] = createSignal("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 (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 + handleResp(resp, (data) => { + notify.success( + t("tasks.path_batch_result", { + count: data?.count ?? 0, + }), + ) + setPathModalOpen(false) + refresh() + }) + } finally { + setPathBatchLoading(false) + } + } const notifyIndividualError = (msg: Record) => { Object.entries(msg).forEach(([key, value]) => { notify.error(`${key}: ${value}`) @@ -326,6 +366,31 @@ export const Tasks = (props: TasksProps) => { > {t(`tasks.${operateName}_selected`)} + + + + + + + { + setPathModalOpen(false)} + loading={pathBatchLoading()} + onSubmit={submitPathBatch} + /> Date: Mon, 27 Jul 2026 23:43:50 +0800 Subject: [PATCH 2/2] fix(tasks): handle normalized paths and batch results - Reject ambiguous administrator inputs that normalize to the root path - Display matched and processed task counts separately - Update the path batch response type and user-facing messages Signed-off-by: ifloppy <68799904+ifloppy@users.noreply.github.com> --- src/lang/en/tasks.json | 3 ++- src/pages/manage/tasks/Tasks.tsx | 23 ++++++++++++++++++++++- src/types/task.ts | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/lang/en/tasks.json b/src/lang/en/tasks.json index 62949445e..5d4578731 100644 --- a/src/lang/en/tasks.json +++ b/src/lang/en/tasks.json @@ -31,7 +31,8 @@ "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_batch_result": "{{count}} task(s) affected", + "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", diff --git a/src/pages/manage/tasks/Tasks.tsx b/src/pages/manage/tasks/Tasks.tsx index 7bd8c51e1..d3cd3fd9c 100644 --- a/src/pages/manage/tasks/Tasks.tsx +++ b/src/pages/manage/tasks/Tasks.tsx @@ -28,6 +28,22 @@ import { me } from "~/store" type PathBatchAction = "delete" | "cancel" | "retry" +const normalizesToRoot = (path: string) => { + const normalized = path + .replaceAll("\\", "/") + .split("/") + .reduce((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 @@ -227,6 +243,10 @@ export const Tasks = (props: TasksProps) => { 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 }), @@ -242,7 +262,8 @@ export const Tasks = (props: TasksProps) => { handleResp(resp, (data) => { notify.success( t("tasks.path_batch_result", { - count: data?.count ?? 0, + matched: data?.matched ?? 0, + processed: data?.processed ?? 0, }), ) setPathModalOpen(false) diff --git a/src/types/task.ts b/src/types/task.ts index c46d3ce2b..6b7847e50 100644 --- a/src/types/task.ts +++ b/src/types/task.ts @@ -13,5 +13,6 @@ export interface TaskInfo { } export interface TaskPathResult { - count: number + matched: number + processed: number }