From 7b97032370a525556d3c6b4a718becca77728ced Mon Sep 17 00:00:00 2001 From: KodekaTeam Date: Thu, 6 Aug 2026 16:13:53 +0700 Subject: [PATCH] feat(s3-storage): standardize validation and improve action menu UI --- .../components/S3StorageSettingsPanel.tsx | 155 ++++++++++-------- src/app/s3-storage/page.tsx | 2 - 2 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/app/s3-storage/components/S3StorageSettingsPanel.tsx b/src/app/s3-storage/components/S3StorageSettingsPanel.tsx index cb846fe..3abbf22 100644 --- a/src/app/s3-storage/components/S3StorageSettingsPanel.tsx +++ b/src/app/s3-storage/components/S3StorageSettingsPanel.tsx @@ -1,12 +1,13 @@ "use client"; -import { useState } from "react"; +import { type MouseEvent, useEffect, useRef, useState } from "react"; import { Cloud, Database, HardDrive, Key, Loader2, + MoreVertical, Pencil, Plus, Power, @@ -213,6 +214,12 @@ export default function S3StorageSettingsPanel({ }: S3StorageSettingsPanelProps) { const [draft, setDraft] = useState(null); const [editingId, setEditingId] = useState(null); + const [openMenuId, setOpenMenuId] = useState(null); + const [menuPosition, setMenuPosition] = useState<{ + top: number; + left: number; + } | null>(null); + const actionMenuRef = useRef(null); const [currentPage, setCurrentPage] = useState(1); const [search, setSearch] = useState(""); @@ -262,6 +269,58 @@ export default function S3StorageSettingsPanel({ setDraft(null); }; + const toggleActionMenu = ( + event: MouseEvent, + destinationId: string, + ) => { + const rect = event.currentTarget.getBoundingClientRect(); + const menuWidth = 176; + const menuHeight = 132; + const left = Math.min( + window.innerWidth - menuWidth - 12, + Math.max(12, rect.right - menuWidth + rect.width), + ); + const top = Math.min(window.innerHeight - menuHeight - 12, rect.bottom + 6); + + if (openMenuId === destinationId) { + setOpenMenuId(null); + setMenuPosition(null); + return; + } + + setMenuPosition({ top: Math.max(12, top), left }); + setOpenMenuId(destinationId); + }; + + const closeActionMenu = () => { + setOpenMenuId(null); + setMenuPosition(null); + }; + + useEffect(() => { + if (!openMenuId) return; + + const closeMenuOnOutsideClick = (event: globalThis.MouseEvent) => { + const target = event.target; + if (!(target instanceof Node) || actionMenuRef.current?.contains(target)) { + return; + } + + const trigger = + target instanceof Element + ? target.closest("[data-action-menu-trigger]") + : null; + if (trigger?.getAttribute("data-action-menu-trigger") === openMenuId) { + return; + } + + closeActionMenu(); + }; + + document.addEventListener("mousedown", closeMenuOnOutsideClick); + return () => document.removeEventListener("mousedown", closeMenuOnOutsideClick); + }, [openMenuId]); + const saveDraft = async () => { if (!draft) return; @@ -514,81 +573,36 @@ export default function S3StorageSettingsPanel({ style={{ display: "flex", justifyContent: "flex-end", - gap: 8, + gap: 6, flexWrap: "wrap", }} > - - + {openMenuId === destination.id && menuPosition ?
event.stopPropagation()} style={{ position: "fixed", top: menuPosition.top, left: menuPosition.left, minWidth: 176, padding: 6, display: "flex", flexDirection: "column", gap: 4, zIndex: 160, boxShadow: "0 18px 40px rgba(2, 6, 23, 0.42)" }}> + + +
: null} @@ -842,7 +856,10 @@ export default function S3StorageSettingsPanel({ />
- Object prefix / folder + + Object prefix / folder + {(draft.retentionDays || draft.maxBackupCount) && " *"} +
- Destination name + Destination name *
- Access Key ID + Access Key ID *
- Secret Access Key + Secret Access Key{!draft.hasSecretAccessKey && " *"} {draft.hasSecretAccessKey ? " (leave blank to keep existing key)" : ""} @@ -1039,7 +1056,7 @@ export default function S3StorageSettingsPanel({
- Bucket + Bucket *
- Region + Region * - Endpoint + Endpoint{draft.provider !== "awsS3" && " *"} {draft.provider === "awsS3" ? " (optional for AWS S3)" : " (required for S3-compatible providers)"} diff --git a/src/app/s3-storage/page.tsx b/src/app/s3-storage/page.tsx index 55a154e..a2a4ea4 100644 --- a/src/app/s3-storage/page.tsx +++ b/src/app/s3-storage/page.tsx @@ -123,7 +123,6 @@ export default function S3StoragePage() { const upsertDestination = async (draft: DestinationDraft) => { const missing = [ !draft.accessKeyId && "Access Key ID", - !draft.secretAccessKey && "Secret Access Key", !draft.region && "Region", !draft.bucket && "Bucket", !draft.name && "Destination Name", @@ -179,7 +178,6 @@ export default function S3StoragePage() { const verifyDestination = async (draft: DestinationDraft) => { const missing = [ !draft.accessKeyId && "Access Key ID", - !draft.secretAccessKey && "Secret Access Key", !draft.region && "Region", !draft.bucket && "Bucket", !draft.name && "Destination Name",