fix: harden browser page webkit compatibility (#133)

This commit is contained in:
cxymds
2026-06-14 21:24:06 +08:00
committed by GitHub
parent c34ef4432f
commit 3977d67fb1
12 changed files with 98 additions and 12 deletions
+2 -1
View File
@@ -20,6 +20,7 @@ import { usePermissions } from "@/hooks/use-permissions"
import { useDialog } from "@/lib/feedback/dialog"
import { useMessage } from "@/lib/feedback/message"
import { niceBytes } from "@/lib/functions"
import { normalizeDateToIso } from "@/lib/safe-date"
import { BrowserContent } from "./content"
import type { ColumnDef } from "@tanstack/react-table"
import dayjs from "dayjs"
@@ -154,7 +155,7 @@ function BrowserBucketsPage() {
const bucketRow: BucketRow = {
Name: name,
CreationDate: item?.CreationDate ? new Date(item.CreationDate).toISOString() : "",
CreationDate: normalizeDateToIso(item?.CreationDate),
}
return bucketRow
+2 -1
View File
@@ -12,6 +12,7 @@ import { useBucket } from "@/hooks/use-bucket"
import { useSystem } from "@/hooks/use-system"
import { Spinner } from "@/components/ui/spinner"
import { niceBytes } from "@/lib/functions"
import { normalizeDateToIso } from "@/lib/safe-date"
import type { ColumnDef } from "@tanstack/react-table"
import dayjs from "dayjs"
@@ -132,7 +133,7 @@ export function BucketList({ title, emptyDescription, getBucketHref }: BucketLis
return {
Name: name,
CreationDate: item?.CreationDate ? new Date(item.CreationDate).toISOString() : "",
CreationDate: normalizeDateToIso(item?.CreationDate),
}
})
.filter((bucket): bucket is BucketListRow => bucket !== null)
+3 -2
View File
@@ -13,6 +13,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"
import { useBucket } from "@/hooks/use-bucket"
import { useEventTarget } from "@/hooks/use-event-target"
import { useMessage } from "@/lib/feedback/message"
import { scheduleMicrotask } from "@/lib/schedule-microtask"
interface EventsNewFormProps {
open: boolean
@@ -67,7 +68,7 @@ export function EventsNewForm({ open, onOpenChange, bucketName, onSuccess, disab
}, [getEventTargetArnList])
useEffect(() => {
queueMicrotask(() => loadArnList())
scheduleMicrotask(() => loadArnList())
}, [loadArnList])
const resetForm = useCallback(() => {
@@ -81,7 +82,7 @@ export function EventsNewForm({ open, onOpenChange, bucketName, onSuccess, disab
useEffect(() => {
if (open) {
queueMicrotask(() => resetForm())
scheduleMicrotask(() => resetForm())
}
}, [open, resetForm])
+2 -1
View File
@@ -22,6 +22,7 @@ import { useMessage } from "@/lib/feedback/message"
import { useDialog } from "@/lib/feedback/dialog"
import { exportFile } from "@/lib/export-file"
import { getContentType } from "@/lib/mime-types"
import { normalizeDateToIso } from "@/lib/safe-date"
import {
getDefaultObjectRetentionDate,
getMinimumObjectRetentionDate,
@@ -431,7 +432,7 @@ export function ObjectInfo({ bucketName, objectKey, open, onOpenChange, onPrevie
}
}
const lastModified = object?.LastModified ? new Date(object.LastModified as string | Date).toISOString() : ""
const lastModified = normalizeDateToIso((object?.LastModified as string | Date | undefined) ?? undefined)
return (
<>
+17 -2
View File
@@ -40,6 +40,7 @@ import { useMessage } from "@/lib/feedback/message"
import { exportFile } from "@/lib/export-file"
import { getContentType } from "@/lib/mime-types"
import { formatBytes } from "@/lib/functions"
import { normalizeDateToIso } from "@/lib/safe-date"
import { buildBucketPath } from "@/lib/bucket-path"
import {
createObjectListScope,
@@ -189,10 +190,24 @@ export function ObjectList({
Key: item.Key ?? "",
type: "object" as const,
Size: item.Size ?? 0,
LastModified: item.LastModified ? item.LastModified.toISOString() : "",
LastModified: normalizeDateToIso(item.LastModified),
}))
setData([...prefixItems, ...objectItems])
} catch (error) {
console.error("Failed to fetch objects:", error)
message.error((error as Error)?.message ?? t("Failed to load objects"))
if (
shouldApplyObjectListResponse({
requestId,
activeRequestId: requestIdRef.current,
requestScope,
activeScope: activeScopeRef.current,
})
) {
setNextToken(undefined)
setData([])
}
} finally {
window.setTimeout(() => {
if (
@@ -208,7 +223,7 @@ export function ObjectList({
}, 200)
}
},
[bucket, prefix, resolvedPageSize, continuationToken, showDeleted, listObject],
[bucket, prefix, resolvedPageSize, continuationToken, showDeleted, listObject, message, t],
)
const prevRefreshTriggerRef = React.useRef(refreshTrigger)
+2 -1
View File
@@ -9,6 +9,7 @@ import { useObject } from "@/hooks/use-object"
import { useMessage } from "@/lib/feedback/message"
import { exportFile } from "@/lib/export-file"
import { getContentType } from "@/lib/mime-types"
import { normalizeDateToIso } from "@/lib/safe-date"
interface ObjectViewProps {
bucketName: string
@@ -49,7 +50,7 @@ export function ObjectView({ bucketName, objectKey }: ObjectViewProps) {
}
}
const lastModified = object?.LastModified ? new Date(object.LastModified as string).toISOString() : ""
const lastModified = normalizeDateToIso((object?.LastModified as string | Date | undefined) ?? undefined)
return (
<div className="space-y-4">
+3 -2
View File
@@ -7,6 +7,7 @@ import { AwsClient } from "@/lib/aws4fetch"
import { ApiErrorHandler } from "@/lib/api-error-handler"
import { useAuth } from "@/contexts/auth-context"
import { configManager } from "@/lib/config"
import { scheduleMicrotask } from "@/lib/schedule-microtask"
interface ApiContextValue {
api: ApiClient | null
@@ -24,14 +25,14 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (!isAuthenticated || !credentials?.AccessKeyId) {
queueMicrotask(() => {
scheduleMicrotask(() => {
setApiClient(null)
setIsReady(true)
})
return
}
queueMicrotask(() => setIsReady(false))
scheduleMicrotask(() => setIsReady(false))
let cancelled = false
configManager.loadConfig().then((siteConfig) => {
+3 -2
View File
@@ -7,6 +7,7 @@ import { useAuth } from "@/contexts/auth-context"
import { addApiPrefixMiddleware } from "@/lib/api-prefix-middleware"
import { configManager } from "@/lib/config"
import { getServiceErrorMessage, getXmlErrorMessage } from "@/lib/error-handler"
import { scheduleMicrotask } from "@/lib/schedule-microtask"
import type { SiteConfig } from "@/types/config"
interface S3Response {
@@ -79,14 +80,14 @@ export function S3Provider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (!isAuthenticated || !credentials?.AccessKeyId) {
queueMicrotask(() => {
scheduleMicrotask(() => {
setS3Client(null)
setIsReady(true)
})
return
}
queueMicrotask(() => setIsReady(false))
scheduleMicrotask(() => setIsReady(false))
let cancelled = false
configManager.loadConfig().then((siteConfig: SiteConfig) => {
+12
View File
@@ -0,0 +1,12 @@
export function normalizeDateToIso(value: Date | string | undefined | null): string {
if (!value) return ""
const date = value instanceof Date ? value : new Date(value)
const time = date.getTime()
if (Number.isNaN(time)) {
return ""
}
return date.toISOString()
}
+13
View File
@@ -0,0 +1,13 @@
export function scheduleMicrotask(callback: () => void) {
if (typeof queueMicrotask === "function") {
queueMicrotask(callback)
return
}
if (typeof Promise === "function") {
void Promise.resolve().then(callback)
return
}
globalThis.setTimeout(callback, 0)
}
+20
View File
@@ -0,0 +1,20 @@
import test from "node:test"
import assert from "node:assert/strict"
import fs from "node:fs"
test("safe date helper returns an empty string for invalid values instead of throwing", () => {
const source = fs.readFileSync("lib/safe-date.ts", "utf8")
assert.match(source, /if \(!value\) return ""/)
assert.match(source, /const date = value instanceof Date \? value : new Date\(value\)/)
assert.match(source, /if \(Number\.isNaN\(time\)\) \{\s+return ""\s+\}/)
assert.match(source, /return date\.toISOString\(\)/)
})
test("scheduleMicrotask falls back when queueMicrotask is unavailable", () => {
const source = fs.readFileSync("lib/schedule-microtask.ts", "utf8")
assert.equal(source.includes('if (typeof queueMicrotask === "function")'), true)
assert.equal(source.includes('if (typeof Promise === "function")'), true)
assert.equal(source.includes("globalThis.setTimeout(callback, 0)"), true)
})
+19
View File
@@ -0,0 +1,19 @@
import test from "node:test"
import assert from "node:assert/strict"
import fs from "node:fs"
test("object list normalizes LastModified through the safe date helper", () => {
const source = fs.readFileSync("components/object/list.tsx", "utf8")
assert.equal(source.includes('import { normalizeDateToIso } from "@/lib/safe-date"'), true)
assert.equal(source.includes("LastModified: normalizeDateToIso(item.LastModified)"), true)
assert.equal(source.includes('item.LastModified ? item.LastModified.toISOString() : ""'), false)
})
test("object list falls back to an empty table instead of crashing the page on fetch errors", () => {
const source = fs.readFileSync("components/object/list.tsx", "utf8")
assert.equal(source.includes('console.error("Failed to fetch objects:", error)'), true)
assert.equal(source.includes('message.error((error as Error)?.message ?? t("Failed to load objects"))'), true)
assert.equal(source.includes("setData([])"), true)
})