From fec6bafcdaa3b8a0f9876eb63ce611db043dd719 Mon Sep 17 00:00:00 2001
From: HyaCinth <88471803+HyaCiovo@users.noreply.github.com>
Date: Fri, 4 Jul 2025 21:53:21 +0800
Subject: [PATCH] refactor(web): Restructure the operation buttons layout in
the app information component (#21742) (#21818)
---
web/app/components/app-sidebar/app-info.tsx | 144 +++++++----------
.../components/app-sidebar/app-operations.tsx | 145 ++++++++++++++++++
2 files changed, 200 insertions(+), 89 deletions(-)
create mode 100644 web/app/components/app-sidebar/app-operations.tsx
diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx
index c6d0e776d..c28cc20df 100644
--- a/web/app/components/app-sidebar/app-info.tsx
+++ b/web/app/components/app-sidebar/app-info.tsx
@@ -10,7 +10,6 @@ import {
RiFileCopy2Line,
RiFileDownloadLine,
RiFileUploadLine,
- RiMoreLine,
} from '@remixicon/react'
import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal'
@@ -35,7 +34,8 @@ import ContentDialog from '@/app/components/base/content-dialog'
import Button from '@/app/components/base/button'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
import Divider from '../base/divider'
-import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem'
+import type { Operation } from './app-operations'
+import AppOperations from './app-operations'
export type IAppInfoProps = {
expand: boolean
@@ -186,14 +186,58 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const { isCurrentWorkspaceEditor } = useAppContext()
- const [showMore, setShowMore] = useState(false)
- const handleTriggerMore = useCallback(() => {
- setShowMore(true)
- }, [setShowMore])
-
if (!appDetail)
return null
+ const operations = [
+ {
+ id: 'edit',
+ title: t('app.editApp'),
+ icon: ,
+ onClick: () => {
+ setOpen(false)
+ onDetailExpand?.(false)
+ setShowEditModal(true)
+ },
+ },
+ {
+ id: 'duplicate',
+ title: t('app.duplicate'),
+ icon: ,
+ onClick: () => {
+ setOpen(false)
+ onDetailExpand?.(false)
+ setShowDuplicateModal(true)
+ },
+ },
+ {
+ id: 'export',
+ title: t('app.export'),
+ icon: ,
+ onClick: exportCheck,
+ },
+ (appDetail.mode !== 'agent-chat' && (appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow')) ? {
+ id: 'import',
+ title: t('workflow.common.importDSL'),
+ icon: ,
+ onClick: () => {
+ setOpen(false)
+ onDetailExpand?.(false)
+ setShowImportDSLModal(true)
+ },
+ } : undefined,
+ (appDetail.mode !== 'agent-chat' && (appDetail.mode === 'completion' || appDetail.mode === 'chat')) ? {
+ id: 'switch',
+ title: t('app.switch'),
+ icon: ,
+ onClick: () => {
+ setOpen(false)
+ onDetailExpand?.(false)
+ setShowSwitchModal(true)
+ },
+ } : undefined,
+ ].filter((op): op is Operation => Boolean(op))
+
return (
{!onlyShowDetail && (
@@ -259,88 +303,10 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
{appDetail.description}
)}
{/* operations */}
-
-
-
-
- {appDetail.mode !== 'agent-chat' &&
-
-
-
-
-
- {
- (appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow')
- &&
{
- setOpen(false)
- onDetailExpand?.(false)
- setShowImportDSLModal(true)
- }}>
-
- {t('workflow.common.importDSL')}
-
- }
- {
- (appDetail.mode === 'completion' || appDetail.mode === 'chat')
- &&
{
- setOpen(false)
- onDetailExpand?.(false)
- setShowSwitchModal(true)
- }}>
-
- {t('app.switch')}
-
- }
-
-
- }
-
+
void
+}
+
+const AppOperations = ({ operations, gap }: {
+ operations: Operation[]
+ gap: number
+}) => {
+ const { t } = useTranslation()
+ const [visibleOpreations, setVisibleOperations] = useState([])
+ const [moreOperations, setMoreOperations] = useState([])
+ const [showMore, setShowMore] = useState(false)
+ const navRef = useRef(null)
+ const handleTriggerMore = useCallback(() => {
+ setShowMore(true)
+ }, [setShowMore])
+
+ useEffect(() => {
+ const moreElement = document.getElementById('more')
+ const navElement = document.getElementById('nav')
+ let width = 0
+ const containerWidth = navElement?.clientWidth ?? 0
+ const moreWidth = moreElement?.clientWidth ?? 0
+
+ if (containerWidth === 0 || moreWidth === 0) return
+
+ const updatedEntries: Record = operations.reduce((pre, cur) => {
+ pre[cur.id] = false
+ return pre
+ }, {} as Record)
+ const childrens = Array.from(navRef.current!.children).slice(0, -1)
+ for (let i = 0; i < childrens.length; i++) {
+ const child: any = childrens[i]
+ const id = child.dataset.targetid
+ if (!id) break
+ const childWidth = child.clientWidth
+
+ if (width + gap + childWidth + moreWidth <= containerWidth) {
+ updatedEntries[id] = true
+ width += gap + childWidth
+ }
+ else {
+ if (i === childrens.length - 1 && width + childWidth <= containerWidth)
+ updatedEntries[id] = true
+ else
+ updatedEntries[id] = false
+ break
+ }
+ }
+ setVisibleOperations(operations.filter(item => updatedEntries[item.id]))
+ setMoreOperations(operations.filter(item => !updatedEntries[item.id]))
+ }, [operations, gap])
+
+ return (
+ <>
+ {!visibleOpreations.length &&
+ {operations.map((operation, index) =>
+ ,
+ )}
+
+
}
+
+ {visibleOpreations.map(operation =>
+
,
+ )}
+ {visibleOpreations.length < operations.length &&
+
+
+
+
+
+ {moreOperations.map(item =>
+ {cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
+ {item.title}
+
)}
+
+
+ }
+
+ >
+ )
+}
+
+export default AppOperations