fix(workflow): correct maximized editor panel layout in execution logs (#34909)

This commit is contained in:
XHamzaX 2026-04-10 11:59:09 +01:00 committed by GitHub
parent 2dc015b360
commit 6612ba69b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 164 additions and 9 deletions

View File

@ -84,7 +84,7 @@ const Base: FC<Props> = ({
return (
<Wrap className={cn(wrapClassName)} style={wrapStyle} isInNode={isInNode} isExpand={isExpand}>
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', !isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}>
<div ref={ref} className={cn(className, isExpand ? 'h-full border-0' : 'rounded-lg border', !isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}>
<div className="flex h-7 items-center justify-between pl-3 pr-2 pt-1">
<div className="system-xs-semibold-uppercase text-text-secondary">{title}</div>
<div

View File

@ -0,0 +1,123 @@
import { act, renderHook } from '@testing-library/react'
import { useRef } from 'react'
import useToggleExpend from '../use-toggle-expend'
type HookProps = {
hasFooter?: boolean
isInNode?: boolean
clientHeight?: number
}
/**
* Wrapper that provides a real ref whose `.current.clientHeight` is stubbed
* so we can verify the height math without a real DOM layout pass.
*/
function useHarness({ hasFooter, isInNode, clientHeight = 400 }: HookProps) {
const ref = useRef<HTMLDivElement | null>(null)
// Stub a ref-like object so measurements are deterministic.
if (!ref.current) {
Object.defineProperty(ref, 'current', {
value: { clientHeight } as HTMLDivElement,
writable: true,
})
}
return useToggleExpend({ ref, hasFooter, isInNode })
}
describe('useToggleExpend', () => {
describe('collapsed state', () => {
it('returns empty wrapClassName and zero expand height when collapsed', () => {
const { result } = renderHook(() => useHarness({ clientHeight: 400 }))
expect(result.current.isExpand).toBe(false)
expect(result.current.wrapClassName).toBe('')
expect(result.current.editorExpandHeight).toBe(0)
})
})
describe('expanded state (node context)', () => {
it('uses fixed positioning inside a workflow node panel', () => {
const { result } = renderHook(() =>
useHarness({ isInNode: true, clientHeight: 400 }),
)
act(() => {
result.current.setIsExpand(true)
})
expect(result.current.isExpand).toBe(true)
expect(result.current.wrapClassName).toContain('fixed')
expect(result.current.wrapClassName).toContain('bg-components-panel-bg')
expect(result.current.wrapStyle).toEqual(
expect.objectContaining({ boxShadow: expect.any(String) }),
)
})
})
describe('expanded state (execution-log / webapp context)', () => {
it('fills its positioned ancestor edge-to-edge without hardcoded offsets', () => {
const { result } = renderHook(() =>
useHarness({ isInNode: false, clientHeight: 400 }),
)
act(() => {
result.current.setIsExpand(true)
})
// The expanded panel must fill the nearest positioned ancestor entirely
// (absolute + inset-0). Previously it used hardcoded `top-[52px]` which
// assumed a 52px header that does not exist in the conversation-log
// layout, causing the expanded panel to overlap the status bar above
// the editor (#34887).
expect(result.current.wrapClassName).toContain('absolute')
expect(result.current.wrapClassName).toContain('inset-0')
expect(result.current.wrapClassName).not.toMatch(/top-\[\d+px\]/)
expect(result.current.wrapClassName).not.toMatch(/left-\d+/)
expect(result.current.wrapClassName).not.toMatch(/right-\d+/)
expect(result.current.wrapClassName).toContain('bg-components-panel-bg')
})
})
describe('expanded state height math', () => {
it('subtracts the 29px chrome when hasFooter is false', () => {
const { result } = renderHook(() =>
useHarness({ hasFooter: false, clientHeight: 400 }),
)
act(() => {
result.current.setIsExpand(true)
})
// 400 (clientHeight) - 29 (title bar) = 371
expect(result.current.editorExpandHeight).toBe(371)
})
it('subtracts the 56px chrome when hasFooter is true', () => {
const { result } = renderHook(() =>
useHarness({ hasFooter: true, clientHeight: 400 }),
)
act(() => {
result.current.setIsExpand(true)
})
// 400 (clientHeight) - 56 (title bar + footer) = 344
expect(result.current.editorExpandHeight).toBe(344)
})
it('never returns a negative height even if chrome exceeds wrap', () => {
const { result } = renderHook(() =>
useHarness({ hasFooter: true, clientHeight: 20 }),
)
act(() => {
result.current.setIsExpand(true)
})
// 20 - 56 would be -36; clamped to 0.
expect(result.current.editorExpandHeight).toBe(0)
})
})
})

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useLayoutEffect, useState } from 'react'
type Params = {
ref?: React.RefObject<HTMLDivElement | null>
@ -6,30 +6,62 @@ type Params = {
isInNode?: boolean
}
// Chrome (title bar + optional footer) heights subtracted from the wrap so
// the editor body never paints underneath its own controls.
const CHROME_HEIGHT_WITH_FOOTER = 56
const CHROME_HEIGHT_WITHOUT_FOOTER = 29
/**
* Controls the expand/collapse behavior of the code editor wrapper used across
* workflow nodes and execution-log panels.
*
* Returns:
* - `wrapClassName` / `wrapStyle` positioning + shadow applied to the outer
* wrapper when the editor is expanded.
* - `editorExpandHeight` height for the editor body (wrap minus chrome).
* - `isExpand` / `setIsExpand` state + setter for the consumer.
*
* Height is measured via `useLayoutEffect` so the first expanded render
* already has the correct value the previous `useEffect` implementation
* left the editor at the collapsed height for one paint on first expand.
*/
const useToggleExpend = ({ ref, hasFooter = true, isInNode }: Params) => {
const [isExpand, setIsExpand] = useState(false)
const [wrapHeight, setWrapHeight] = useState(ref?.current?.clientHeight)
const editorExpandHeight = isExpand ? wrapHeight! - (hasFooter ? 56 : 29) : 0
useEffect(() => {
const [wrapHeight, setWrapHeight] = useState<number | undefined>(undefined)
useLayoutEffect(() => {
if (!ref?.current)
return
setWrapHeight(ref.current?.clientHeight)
}, [isExpand])
setWrapHeight(ref.current.clientHeight)
}, [isExpand, ref])
const chromeHeight = hasFooter ? CHROME_HEIGHT_WITH_FOOTER : CHROME_HEIGHT_WITHOUT_FOOTER
const editorExpandHeight = isExpand && wrapHeight !== undefined
? Math.max(0, wrapHeight - chromeHeight)
: 0
const wrapClassName = (() => {
if (!isExpand)
return ''
if (isInNode)
return 'fixed z-10 right-[9px] top-[166px] bottom-[8px] p-4 bg-components-panel-bg rounded-xl'
return 'fixed z-10 right-[9px] top-[166px] bottom-[8px] p-4 bg-components-panel-bg rounded-xl'
return 'absolute z-10 left-4 right-6 top-[52px] bottom-0 pb-4 bg-components-panel-bg'
// Fill the nearest positioned ancestor entirely. Previously hardcoded
// `top-[52px] left-4 right-6` offsets assumed a 52px header above the
// scroll container — that assumption no longer holds in the conversation
// log (result-panel) layout, where the status bar above the editor is
// taller than 52px, causing the expanded panel to partially overlap the
// status bar (issue #34887).
return 'absolute z-10 inset-0 pb-4 bg-components-panel-bg'
})()
const wrapStyle = isExpand
? {
boxShadow: '0px 0px 12px -4px rgba(16, 24, 40, 0.05), 0px -3px 6px -2px rgba(16, 24, 40, 0.03)',
}
: {}
return {
wrapClassName,
wrapStyle,