Merge branch 'main' of github.com:rustfs/console

This commit is contained in:
马登山
2026-02-27 18:58:11 +08:00
7 changed files with 89 additions and 7 deletions
+32 -1
View File
@@ -13,6 +13,7 @@ import { ObjectUploadPicker } from "@/components/object/upload-picker"
import { useBucket } from "@/hooks/use-bucket"
import { useMessage } from "@/lib/feedback/message"
import { buildBucketPath } from "@/lib/bucket-path"
import { useTasks } from "@/contexts/task-context"
interface BrowserContentProps {
bucketName: string
@@ -100,6 +101,37 @@ export function BrowserContent({ bucketName, keyPath = "", preview = false, prev
setRefreshTrigger((n) => n + 1)
}
const tasks = useTasks()
const debounceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
const prevCompletedIdsRef = React.useRef(new Set<string>())
React.useEffect(() => {
const currentIds = new Set(tasks.map((t) => t.id))
for (const id of prevCompletedIdsRef.current) {
if (!currentIds.has(id)) prevCompletedIdsRef.current.delete(id)
}
const completedForBucket = tasks.filter(
(t) =>
(t.kind === "upload" || t.kind === "delete") &&
t.bucketName === bucketName &&
t.status === "completed",
)
const newCompletions = completedForBucket.filter((t) => !prevCompletedIdsRef.current.has(t.id))
if (newCompletions.length > 0) {
newCompletions.forEach((t) => prevCompletedIdsRef.current.add(t.id))
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
debounceTimerRef.current = setTimeout(() => {
setRefreshTrigger((n) => n + 1)
}, 1500)
}
}, [tasks, bucketName, setRefreshTrigger])
React.useEffect(() => {
return () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
}
}, [])
return (
<Page>
<PageHeader>
@@ -152,7 +184,6 @@ export function BrowserContent({ bucketName, keyPath = "", preview = false, prev
onShowChange={setUploadPickerOpen}
bucketName={bucketName}
prefix={prefix}
onSuccess={handleRefresh}
/>
</Page>
)
+2 -2
View File
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"
import { RiFileCopyLine } from "@remixicon/react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { copyToClipboard } from "@/lib/clipboard"
import { useMessage } from "@/lib/feedback/message"
import { cn } from "@/lib/utils"
@@ -29,8 +30,7 @@ export function CopyInput({
const handleCopy = async () => {
try {
if (!value) throw new Error("No value to copy")
await navigator.clipboard.writeText(value)
await copyToClipboard(value)
message.success(t("Copy Success"))
} catch {
message.error(t("Copy Failed"))
-2
View File
@@ -438,7 +438,6 @@ export function ObjectList({
message.success(t("Delete task created"))
}
table.resetRowSelection()
;(onRefresh ?? fetchObjects)()
} catch (err) {
message.error((err as Error)?.message ?? t("Delete Failed"))
}
@@ -454,7 +453,6 @@ export function ObjectList({
message.success(t("Delete task created"))
}
table.resetRowSelection()
;(onRefresh ?? fetchObjects)()
} catch (err) {
message.error((err as Error)?.message ?? t("Delete Failed"))
}
+2 -1
View File
@@ -3,6 +3,7 @@
import * as React from "react"
import { useTranslation } from "react-i18next"
import { Button } from "@/components/ui/button"
import { copyToClipboard } from "@/lib/clipboard"
import { RiFileCopyLine, RiCheckLine } from "@remixicon/react"
interface Segment {
@@ -21,7 +22,7 @@ function useClipboard(value: string, copiedDuring = 3000) {
const copy = React.useCallback(async () => {
try {
await navigator.clipboard.writeText(value)
await copyToClipboard(value)
setCopied(true)
setTimeout(() => setCopied(false), copiedDuring)
} catch {
+2 -1
View File
@@ -9,6 +9,7 @@ import { DataTable } from "@/components/data-table/data-table"
import { useDataTable } from "@/hooks/use-data-table"
import { useObject } from "@/hooks/use-object"
import { useMessage } from "@/lib/feedback/message"
import { copyToClipboard } from "@/lib/clipboard"
import { exportFile } from "@/lib/export-file"
import { getContentType } from "@/lib/mime-types"
import { formatBytes } from "@/lib/functions"
@@ -72,7 +73,7 @@ export function ObjectVersions({
async (versionId: string) => {
if (!versionId) return
try {
await navigator.clipboard.writeText(versionId)
await copyToClipboard(versionId)
message.success(t("Copy Success"))
} catch {
message.error(t("Copy Failed"))
+1
View File
@@ -27,6 +27,7 @@ export type AnyTask = {
displayName: string
subInfo: string
actionLabel: string
bucketName?: string
}
const emptyTasks: AnyTask[] = []
+50
View File
@@ -0,0 +1,50 @@
export async function copyToClipboard(value: string): Promise<void> {
if (!value) throw new Error("No value to copy")
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(value)
return
} catch (error) {
// Fallback to legacy copy only when Clipboard API is denied or unsupported in current context
const fallback = legacyCopyToClipboard(value)
if (fallback) return
if (error instanceof Error) throw error
throw new Error("Failed to copy text")
}
}
const fallback = legacyCopyToClipboard(value)
if (fallback) return
throw new Error("Failed to copy text")
}
function legacyCopyToClipboard(value: string): boolean {
if (typeof document === "undefined" || !document?.execCommand) return false
const textarea = document.createElement("textarea")
textarea.value = value
textarea.setAttribute("readonly", "")
textarea.style.position = "fixed"
textarea.style.top = "0"
textarea.style.left = "0"
textarea.style.opacity = "0"
textarea.style.pointerEvents = "none"
textarea.style.zIndex = "-1"
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
let copied = false
try {
copied = document.execCommand("copy")
} catch {
copied = false
} finally {
document.body.removeChild(textarea)
}
return copied
}