Skip to content

Commit 85f5465

Browse files
authored
refactor: harden task management and TypeScript safety (#162)
1 parent 8fa82a2 commit 85f5465

37 files changed

Lines changed: 1084 additions & 351 deletions

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,7 @@ jobs:
6363
run: |
6464
echo "## 🧪 Running Full Test Suite" >> $GITHUB_STEP_SUMMARY
6565
echo "" >> $GITHUB_STEP_SUMMARY
66-
if [ -d "tests" ] && [ -n "$(find tests -name '*.test.ts' -o -name '*.spec.ts' 2>/dev/null)" ]; then
67-
pnpm run test:run 2>/dev/null || true
68-
fi
66+
pnpm run test:run
6967
echo "✅ All tests passed!" >> $GITHUB_STEP_SUMMARY
7068
7169
- name: 🔍 Validate code quality

‎.github/workflows/test.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
run: pnpm ci
7474

7575
- name: 🧪 Run unit tests
76-
run: pnpm run test:run 2>/dev/null || echo "No test:run script, skipping"
76+
run: pnpm run test:run
7777

7878
- name: 📊 Upload test results
7979
uses: actions/upload-artifact@v7

‎app/(dashboard)/access-keys/page.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,16 +184,19 @@ export default function AccessKeysPage() {
184184
positiveText: t("Confirm"),
185185
negativeText: t("Cancel"),
186186
onPositiveClick: async () => {
187-
try {
188-
await Promise.all(selectedKeys.map((key) => deleteServiceAccount(key)))
187+
const results = await Promise.allSettled(selectedKeys.map((key) => deleteServiceAccount(key)))
188+
const failures = results.filter((result) => result.status === "rejected")
189+
190+
if (failures.length === 0) {
189191
message.success(t("Delete Success"))
190192
table.resetRowSelection()
191-
await listUserAccounts()
192-
} catch (error) {
193+
} else {
194+
const firstFailure = failures[0]
195+
const error = firstFailure?.status === "rejected" ? firstFailure.reason : undefined
193196
console.error(error)
194-
const msg = (error as Error)?.message || t("Delete Failed")
195-
message.error(msg)
197+
message.error((error as Error)?.message || t("Delete Failed"))
196198
}
199+
await listUserAccounts()
197200
},
198201
})
199202
}

‎app/(dashboard)/users/page.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,19 @@ export default function UsersPage() {
199199
positiveText: t("Confirm"),
200200
negativeText: t("Cancel"),
201201
onPositiveClick: async () => {
202-
try {
203-
await Promise.all(selectedKeys.map((key) => deleteUser(key)))
202+
const results = await Promise.allSettled(selectedKeys.map((key) => deleteUser(key)))
203+
const failures = results.filter((result) => result.status === "rejected")
204+
205+
if (failures.length === 0) {
204206
message.success(t("Delete Success"))
205207
table.resetRowSelection()
206-
await getDataList()
207-
} catch (error) {
208+
} else {
209+
const firstFailure = failures[0]
210+
const error = firstFailure?.status === "rejected" ? firstFailure.reason : undefined
208211
console.error(error)
209-
const msg = (error as Error)?.message || t("Delete Failed")
210-
message.error(msg)
212+
message.error((error as Error)?.message || t("Delete Failed"))
211213
}
214+
await getDataList()
212215
},
213216
})
214217
}

‎components/license/enterprise-section.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export function LicenseEnterpriseSection() {
210210
window.open(
211211
"https://ww18.53kf.com/webCompany.php?arg=11003151&kf_sign=DA4MDMTc0Ng4MjE1MjEzODAyNDkyMDAyNzMwMDMxNTE%253D&style=2",
212212
"_blank",
213+
"noopener,noreferrer",
213214
)
214215
}
215216

‎components/object/list.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ export function ObjectList({
375375
try {
376376
const url = await getSignedUrl(key)
377377
const response = await fetch(url)
378+
if (!response.ok) throw new Error(t("Download Failed"))
378379
const filename = key.split("/").pop() ?? ""
379380
const headers: Record<string, string> = {
380381
"content-type": getContentType(response.headers, filename),

‎components/object/preview-modal.tsx‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,24 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
160160

161161
React.useEffect(() => {
162162
if (show && previewMode === "text" && previewUrl) {
163+
const controller = new AbortController()
163164
setLoading(true)
164165
setIsFormatted(true)
165-
fetch(previewUrl)
166-
.then((r) => r.text())
166+
setTextContent("")
167+
fetch(previewUrl, { signal: controller.signal })
168+
.then((response) => {
169+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
170+
return response.text()
171+
})
167172
.then(setTextContent)
168-
.catch(() => setTextContent(t("Preview unavailable")))
169-
.finally(() => setLoading(false))
173+
.catch((error: unknown) => {
174+
if ((error as Error)?.name !== "AbortError") setTextContent(t("Preview unavailable"))
175+
})
176+
.finally(() => {
177+
if (!controller.signal.aborted) setLoading(false)
178+
})
179+
180+
return () => controller.abort()
170181
} else if (!show) {
171182
setTextContent("")
172183
setLoading(false)

‎components/tasks/item.tsx‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import * as React from "react"
44
import { useTranslation } from "react-i18next"
5-
import { RiDeleteBinLine } from "@remixicon/react"
5+
import { RiCloseCircleLine, RiDeleteBinLine } from "@remixicon/react"
66
import { Button } from "@/components/ui/button"
77
import { Progress } from "@/components/ui/progress"
88
import { useTaskManager } from "@/contexts/task-context"
@@ -17,7 +17,6 @@ const statusMap: Record<string, string> = {
1717
running: "in progress",
1818
completed: "success",
1919
failed: "failed",
20-
paused: "paused",
2120
canceled: "canceled",
2221
}
2322

@@ -27,20 +26,35 @@ export function TaskItem({ task }: TaskItemProps) {
2726

2827
const statusKey = statusMap[task.status] ?? "in progress"
2928
const statusText = `${t(task.actionLabel)}${t(statusKey)}`
29+
const isActive = task.status === "pending" || task.status === "running"
30+
const isFinished = task.status === "completed" || task.status === "failed" || task.status === "canceled"
3031

3132
return (
3233
<div className="flex flex-col gap-1">
3334
<div className="flex items-center justify-between gap-3 group">
3435
<div className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">{task.displayName}</div>
35-
<Button
36-
variant="ghost"
37-
size="sm"
38-
className="h-auto shrink-0 px-2 text-xs opacity-0 group-hover:opacity-100"
39-
aria-label={t("Delete Record")}
40-
onClick={() => taskManager.removeTask(task.id)}
41-
>
42-
<RiDeleteBinLine className="size-4 text-destructive" aria-hidden />
43-
</Button>
36+
{isActive && (
37+
<Button
38+
variant="ghost"
39+
size="sm"
40+
className="h-auto shrink-0 px-2 text-xs opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
41+
aria-label={t("Cancel")}
42+
onClick={() => taskManager.cancelTask(task.id)}
43+
>
44+
<RiCloseCircleLine className="size-4" aria-hidden />
45+
</Button>
46+
)}
47+
{isFinished && (
48+
<Button
49+
variant="ghost"
50+
size="sm"
51+
className="h-auto shrink-0 px-2 text-xs opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
52+
aria-label={t("Delete Record")}
53+
onClick={() => taskManager.removeFinishedTask(task.id)}
54+
>
55+
<RiDeleteBinLine className="size-4 text-destructive" aria-hidden />
56+
</Button>
57+
)}
4458
</div>
4559
<Progress value={task.progress} className="h-[2px]" />
4660
<div className="flex items-center justify-between text-xs text-muted-foreground">

‎components/tasks/panel.tsx‎

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,42 @@ import { TaskItem } from "./item"
1010
import type { AnyTask } from "@/contexts/task-context"
1111

1212
interface TaskPanelProps {
13-
tasks: AnyTask[]
14-
onClearTasks: () => void
13+
tasks: readonly AnyTask[]
14+
onClearFinishedTasks: () => void
1515
}
1616

17-
export function TaskPanel({ tasks, onClearTasks }: TaskPanelProps) {
17+
export function TaskPanel({ tasks, onClearFinishedTasks }: TaskPanelProps) {
1818
const { t } = useTranslation()
1919
const [tab, setTab] = React.useState("pending")
2020

2121
const pending = tasks.filter((task) => task.status === "pending")
2222
const processing = tasks.filter((task) => task.status === "running")
2323
const completed = tasks.filter((task) => task.status === "completed")
2424
const failed = tasks.filter((task) => task.status === "failed")
25+
const canceled = tasks.filter((task) => task.status === "canceled")
2526

2627
const total = tasks.length
27-
const percentage = total === 0 ? 100 : Math.floor((completed.length / total) * 100)
28+
const finished = completed.length + failed.length + canceled.length
29+
const percentage = total === 0 ? 100 : Math.floor((finished / total) * 100)
2830

2931
React.useEffect(() => {
3032
if (processing.length > 0) setTab("processing")
3133
else if (pending.length > 0) setTab("pending")
3234
else if (completed.length > 0) setTab("completed")
35+
else if (failed.length > 0) setTab("failed")
36+
else if (canceled.length > 0) setTab("canceled")
3337
else setTab("pending")
34-
}, [processing.length, pending.length, completed.length])
35-
36-
const withCount = (key: string, count: number) => t(key).replace(/\{[^}]*\}/g, String(count))
38+
}, [processing.length, pending.length, completed.length, failed.length, canceled.length])
3739

3840
const tabs = [
39-
{ value: "pending", label: withCount("Pending", pending.length) },
40-
{ value: "processing", label: withCount("Processing (with count)", processing.length) },
41-
{ value: "completed", label: withCount("Completed", completed.length) },
42-
{ value: "failed", label: withCount("Failed", failed.length) },
41+
{ value: "pending", label: t("Pending", { count: pending.length }) },
42+
{ value: "processing", label: t("Processing (with count)", { count: processing.length }) },
43+
{ value: "completed", label: t("Completed", { count: completed.length }) },
44+
{ value: "failed", label: t("Failed", { count: failed.length }) },
45+
{ value: "canceled", label: `${t("Canceled")} (${canceled.length})` },
4346
]
4447

45-
const renderList = (list: AnyTask[]) => (
48+
const renderList = (list: readonly AnyTask[]) => (
4649
<div className="flex flex-col gap-3">
4750
{list.length === 0 ? (
4851
<p className="py-4 text-center text-sm text-muted-foreground">{t("No Data")}</p>
@@ -69,11 +72,13 @@ export function TaskPanel({ tasks, onClearTasks }: TaskPanelProps) {
6972
<div className="space-y-3">
7073
<div className="flex items-center justify-between text-sm text-muted-foreground">
7174
<div>
72-
{total - pending.length - processing.length}/{total}
75+
{finished}/{total}
7376
</div>
74-
<Button variant="ghost" size="sm" className="h-auto px-2" onClick={onClearTasks}>
75-
{t("Clear Records")}
76-
</Button>
77+
{finished > 0 && (
78+
<Button variant="ghost" size="sm" className="h-auto px-2" onClick={onClearFinishedTasks}>
79+
{t("Clear Records")}
80+
</Button>
81+
)}
7782
</div>
7883
<Progress value={percentage} className="h-[3px]" />
7984
</div>
@@ -100,6 +105,9 @@ export function TaskPanel({ tasks, onClearTasks }: TaskPanelProps) {
100105
<TabsContent value="failed" className="mt-0">
101106
{renderList(failed)}
102107
</TabsContent>
108+
<TabsContent value="canceled" className="mt-0">
109+
{renderList(canceled)}
110+
</TabsContent>
103111
</Tabs>
104112
</div>
105113
)

‎components/tasks/stats-button.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@ export function TaskStatsButton() {
1515
const taskManager = useTaskManager()
1616
const { isTaskPanelOpen, setTaskPanelOpen } = useTaskPanelOpen()
1717

18+
const pending = tasks.filter((task) => task.status === "pending")
1819
const processing = tasks.filter((task) => task.status === "running")
1920
const completed = tasks.filter((task) => task.status === "completed")
2021
const failed = tasks.filter((task) => task.status === "failed")
22+
const active = pending.length + processing.length
2123
const total = tasks.length
2224

2325
if (total === 0) return null
2426

2527
return (
2628
<Drawer open={isTaskPanelOpen} onOpenChange={setTaskPanelOpen} swipeDirection="right">
2729
<DrawerTrigger render={<Button variant="outline" />}>
28-
{processing.length > 0 ? (
30+
{active > 0 ? (
2931
<div className="flex items-center gap-2">
3032
<Spinner className="size-3 text-muted-foreground" />
3133
<span>
@@ -48,7 +50,7 @@ export function TaskStatsButton() {
4850
<DrawerTitle>{t("Task Management")}</DrawerTitle>
4951
</DrawerHeader>
5052
<div className="overflow-auto p-4">
51-
<TaskPanel tasks={tasks} onClearTasks={() => taskManager.clearTasks()} />
53+
<TaskPanel tasks={tasks} onClearFinishedTasks={() => taskManager.clearFinishedTasks()} />
5254
</div>
5355
</DrawerContent>
5456
</Drawer>

0 commit comments

Comments
 (0)