From fb3699ec5e03785319c1f910e522681892d200a0 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 5 Nov 2025 20:08:59 +0800 Subject: [PATCH] fix(web): resolve type checks in app operations --- .../components/app-sidebar/app-operations.tsx | 227 ++++++++++++------ 1 file changed, 148 insertions(+), 79 deletions(-) diff --git a/web/app/components/app-sidebar/app-operations.tsx b/web/app/components/app-sidebar/app-operations.tsx index ab1069a29f..ca0ff85316 100644 --- a/web/app/components/app-sidebar/app-operations.tsx +++ b/web/app/components/app-sidebar/app-operations.tsx @@ -1,19 +1,33 @@ import type { JSX } from 'react' -import { cloneElement, useCallback } from 'react' -import { useEffect, useRef, useState } from 'react' +import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem' import { RiMoreLine } from '@remixicon/react' export type Operation = { - id: string; title: string; icon: JSX.Element; onClick: () => void + id: string + title: string + icon: JSX.Element + onClick: () => void + type?: 'divider' } -const AppOperations = ({ operations, gap }: { - operations: Operation[] +type AppOperationsProps = { gap: number -}) => { + operations?: Operation[] + primaryOperations?: Operation[] + secondaryOperations?: Operation[] +} + +const EMPTY_OPERATIONS: Operation[] = [] + +const AppOperations = ({ + operations, + primaryOperations, + secondaryOperations, + gap, +}: AppOperationsProps) => { const { t } = useTranslation() const [visibleOpreations, setVisibleOperations] = useState([]) const [moreOperations, setMoreOperations] = useState([]) @@ -23,22 +37,59 @@ const AppOperations = ({ operations, gap }: { setShowMore(true) }, [setShowMore]) + const primaryOps = useMemo(() => { + if (operations) + return operations + if (primaryOperations) + return primaryOperations + return EMPTY_OPERATIONS + }, [operations, primaryOperations]) + + const secondaryOps = useMemo(() => { + if (operations) + return EMPTY_OPERATIONS + if (secondaryOperations) + return secondaryOperations + return EMPTY_OPERATIONS + }, [operations, secondaryOperations]) + const inlineOperations = primaryOps.filter(operation => operation.type !== 'divider') + useEffect(() => { - const moreElement = document.getElementById('more') - const navElement = document.getElementById('nav') + const applyState = (visible: Operation[], overflow: Operation[]) => { + const combinedMore = [...overflow, ...secondaryOps] + if (!overflow.length && combinedMore[0]?.type === 'divider') + combinedMore.shift() + setVisibleOperations(visible) + setMoreOperations(combinedMore) + } + + const inline = primaryOps.filter(operation => operation.type !== 'divider') + + if (!inline.length) { + applyState([], []) + return + } + + const navElement = navRef.current + const moreElement = document.getElementById('more-measure') + + if (!navElement || !moreElement) + return + let width = 0 - const containerWidth = navElement?.clientWidth ?? 0 - const moreWidth = moreElement?.clientWidth ?? 0 + const containerWidth = navElement.clientWidth + const moreWidth = moreElement.clientWidth - if (containerWidth === 0 || moreWidth === 0) return + if (containerWidth === 0 || moreWidth === 0) + return - const updatedEntries: Record = operations.reduce((pre, cur) => { + const updatedEntries: Record = inline.reduce((pre, cur) => { pre[cur.id] = false return pre }, {} as Record) - const childrens = Array.from(navRef.current!.children).slice(0, -1) + const childrens = Array.from(navElement.children).slice(0, -1) for (let i = 0; i < childrens.length; i++) { - const child: any = childrens[i] + const child = childrens[i] as HTMLElement const id = child.dataset.targetid if (!id) break const childWidth = child.clientWidth @@ -55,88 +106,106 @@ const AppOperations = ({ operations, gap }: { break } } - setVisibleOperations(operations.filter(item => updatedEntries[item.id])) - setMoreOperations(operations.filter(item => !updatedEntries[item.id])) - }, [operations, gap]) + + const visible = inline.filter(item => updatedEntries[item.id]) + const overflow = inline.filter(item => !updatedEntries[item.id]) + + applyState(visible, overflow) + }, [gap, primaryOps, secondaryOps]) + + const shouldShowMoreButton = moreOperations.length > 0 return ( <> - {!visibleOpreations.length && } -
- {visibleOpreations.map(operation => + {inlineOperations.map(operation => ( , - )} - {visibleOpreations.length < operations.length && - - - - -
- {moreOperations.map(item =>
+ ))} + +
+
+ {visibleOpreations.map(operation => ( + + ))} + {shouldShowMoreButton && ( + + +
)} -
-
-
} + + + {t('common.operation.more')} + + + + +
+ {moreOperations.map(item => item.type === 'divider' + ? ( +
+ ) + : ( +
+ {cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })} + {item.title} +
+ ))} +
+ + + )}
)