mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
fix(copilot-autolayout): more subflow cases and deal with resizing (#2236)
* fix sidebar hydration issue accurately * improve autolayout subflow cases * fix DOM structure to prevent HMR hydration issues
This commit is contained in:
committed by
GitHub
parent
9f884c151c
commit
05022e3468
@@ -152,7 +152,23 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
return NextResponse.json({ data: finalWorkflowData }, { status: 200 })
|
||||
}
|
||||
return NextResponse.json({ error: 'Workflow has no normalized data' }, { status: 400 })
|
||||
|
||||
const emptyWorkflowData = {
|
||||
...workflowData,
|
||||
state: {
|
||||
deploymentStatuses: {},
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: workflowData.isDeployed || false,
|
||||
deployedAt: workflowData.deployedAt,
|
||||
},
|
||||
variables: workflowData.variables || {},
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: emptyWorkflowData }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
const elapsed = Date.now() - startTime
|
||||
logger.error(`[${requestId}] Error fetching workflow ${workflowId} after ${elapsed}ms`, error)
|
||||
|
||||
@@ -14,12 +14,14 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
||||
<ProviderModelsLoader />
|
||||
<GlobalCommandsProvider>
|
||||
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
|
||||
<WorkspacePermissionsProvider>
|
||||
<div className='flex min-h-screen w-full'>
|
||||
<SidebarNew />
|
||||
<div className='flex flex-1 flex-col'>{children}</div>
|
||||
</div>
|
||||
</WorkspacePermissionsProvider>
|
||||
<div className='flex min-h-screen w-full'>
|
||||
<WorkspacePermissionsProvider>
|
||||
<div className='shrink-0' suppressHydrationWarning>
|
||||
<SidebarNew />
|
||||
</div>
|
||||
{children}
|
||||
</WorkspacePermissionsProvider>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
</GlobalCommandsProvider>
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export default function TemplatesLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div>{children}</div>
|
||||
return (
|
||||
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
|
||||
<div>{children}</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
|
||||
|
||||
export default function WorkflowLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className='h-full overflow-hidden bg-muted/40'>
|
||||
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -1295,14 +1295,18 @@ const WorkflowContent = React.memo(() => {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we encountered an error loading this specific workflow to prevent infinite retries
|
||||
const hasLoadError = hydration.phase === 'error' && hydration.workflowId === currentId
|
||||
|
||||
// Check if we need to load the workflow state:
|
||||
// 1. Different workflow than currently active
|
||||
// 2. Same workflow but hydration phase is not 'ready' (e.g., after a quick refresh)
|
||||
const needsWorkflowLoad =
|
||||
activeWorkflowId !== currentId ||
|
||||
(activeWorkflowId === currentId &&
|
||||
hydration.phase !== 'ready' &&
|
||||
hydration.phase !== 'state-loading')
|
||||
!hasLoadError &&
|
||||
(activeWorkflowId !== currentId ||
|
||||
(activeWorkflowId === currentId &&
|
||||
hydration.phase !== 'ready' &&
|
||||
hydration.phase !== 'state-loading'))
|
||||
|
||||
if (needsWorkflowLoad) {
|
||||
const { clearDiff } = useWorkflowDiffStore.getState()
|
||||
|
||||
@@ -60,11 +60,15 @@ export function SidebarNew() {
|
||||
// Session data
|
||||
const { data: sessionData, isPending: sessionLoading } = useSession()
|
||||
|
||||
// Sidebar state
|
||||
const isCollapsed = useSidebarStore((state) => state.isCollapsed)
|
||||
// Sidebar state - use store's hydration tracking to prevent SSR mismatch
|
||||
const hasHydrated = useSidebarStore((state) => state._hasHydrated)
|
||||
const isCollapsedStore = useSidebarStore((state) => state.isCollapsed)
|
||||
const setIsCollapsed = useSidebarStore((state) => state.setIsCollapsed)
|
||||
const setSidebarWidth = useSidebarStore((state) => state.setSidebarWidth)
|
||||
|
||||
// Use default (expanded) state until hydrated to prevent hydration mismatch
|
||||
const isCollapsed = hasHydrated ? isCollapsedStore : false
|
||||
|
||||
// Determine if we're on a workflow page (only workflow pages allow collapse and resize)
|
||||
const isOnWorkflowPage = !!workflowId
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -14,21 +14,12 @@ export default function WorkflowsPage() {
|
||||
const { workflows, setActiveWorkflow } = useWorkflowRegistry()
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
// Fetch workflows using React Query
|
||||
const { isLoading, isError } = useWorkflows(workspaceId)
|
||||
|
||||
// Track when component is mounted to avoid hydration issues
|
||||
// Handle redirection once workflows are loaded
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
// Handle redirection once workflows are loaded and component is mounted
|
||||
useEffect(() => {
|
||||
// Wait for component to be mounted to avoid hydration mismatches
|
||||
if (!isMounted) return
|
||||
|
||||
// Only proceed if workflows are done loading
|
||||
if (isLoading) return
|
||||
|
||||
@@ -50,17 +41,19 @@ export default function WorkflowsPage() {
|
||||
const firstWorkflowId = workspaceWorkflows[0]
|
||||
router.replace(`/workspace/${workspaceId}/w/${firstWorkflowId}`)
|
||||
}
|
||||
}, [isMounted, isLoading, workflows, workspaceId, router, setActiveWorkflow, isError])
|
||||
}, [isLoading, workflows, workspaceId, router, setActiveWorkflow, isError])
|
||||
|
||||
// Always show loading state until redirect happens
|
||||
// There should always be a default workflow, so we never show "no workflows found"
|
||||
return (
|
||||
<div className='flex h-screen items-center justify-center'>
|
||||
<div className='text-center'>
|
||||
<div className='mx-auto mb-4'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
|
||||
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<div className='text-center'>
|
||||
<div className='mx-auto mb-4'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,9 @@ import {
|
||||
CONTAINER_LAYOUT_OPTIONS,
|
||||
DEFAULT_LAYOUT_OPTIONS,
|
||||
MAX_OVERLAP_ITERATIONS,
|
||||
OVERLAP_MARGIN,
|
||||
} from '@/lib/workflows/autolayout/constants'
|
||||
import type { Edge, GraphNode, LayoutOptions } from '@/lib/workflows/autolayout/types'
|
||||
import {
|
||||
boxesOverlap,
|
||||
createBoundingBox,
|
||||
getBlockMetrics,
|
||||
normalizePositions,
|
||||
prepareBlockMetrics,
|
||||
@@ -172,11 +169,10 @@ export function groupByLayer(nodes: Map<string, GraphNode>): Map<number, GraphNo
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves overlaps between all nodes, including across layers.
|
||||
* Nodes in the same layer are shifted vertically to avoid overlap.
|
||||
* Nodes in different layers that overlap are shifted down.
|
||||
* Resolves vertical overlaps between nodes in the same layer.
|
||||
* X overlaps are prevented by construction via cumulative width-based positioning.
|
||||
*/
|
||||
function resolveOverlaps(nodes: GraphNode[], verticalSpacing: number): void {
|
||||
function resolveVerticalOverlaps(nodes: GraphNode[], verticalSpacing: number): void {
|
||||
let iteration = 0
|
||||
let hasOverlap = true
|
||||
|
||||
@@ -184,42 +180,37 @@ function resolveOverlaps(nodes: GraphNode[], verticalSpacing: number): void {
|
||||
hasOverlap = false
|
||||
iteration++
|
||||
|
||||
// Sort nodes by layer then by Y position for consistent processing
|
||||
const sortedNodes = [...nodes].sort((a, b) => {
|
||||
if (a.layer !== b.layer) return a.layer - b.layer
|
||||
return a.position.y - b.position.y
|
||||
})
|
||||
// Group nodes by layer for same-layer overlap resolution
|
||||
const nodesByLayer = new Map<number, GraphNode[]>()
|
||||
for (const node of nodes) {
|
||||
if (!nodesByLayer.has(node.layer)) {
|
||||
nodesByLayer.set(node.layer, [])
|
||||
}
|
||||
nodesByLayer.get(node.layer)!.push(node)
|
||||
}
|
||||
|
||||
for (let i = 0; i < sortedNodes.length; i++) {
|
||||
for (let j = i + 1; j < sortedNodes.length; j++) {
|
||||
const node1 = sortedNodes[i]
|
||||
const node2 = sortedNodes[j]
|
||||
// Process each layer independently
|
||||
for (const [layer, layerNodes] of nodesByLayer) {
|
||||
if (layerNodes.length < 2) continue
|
||||
|
||||
const box1 = createBoundingBox(node1.position, node1.metrics)
|
||||
const box2 = createBoundingBox(node2.position, node2.metrics)
|
||||
// Sort by Y position for consistent processing
|
||||
layerNodes.sort((a, b) => a.position.y - b.position.y)
|
||||
|
||||
// Check for overlap with margin
|
||||
if (boxesOverlap(box1, box2, OVERLAP_MARGIN)) {
|
||||
for (let i = 0; i < layerNodes.length - 1; i++) {
|
||||
const node1 = layerNodes[i]
|
||||
const node2 = layerNodes[i + 1]
|
||||
|
||||
const node1Bottom = node1.position.y + node1.metrics.height
|
||||
const requiredY = node1Bottom + verticalSpacing
|
||||
|
||||
if (node2.position.y < requiredY) {
|
||||
hasOverlap = true
|
||||
node2.position.y = requiredY
|
||||
|
||||
// If in same layer, shift vertically around midpoint
|
||||
if (node1.layer === node2.layer) {
|
||||
const midpoint = (node1.position.y + node2.position.y) / 2
|
||||
|
||||
node1.position.y = midpoint - node1.metrics.height / 2 - verticalSpacing / 2
|
||||
node2.position.y = midpoint + node2.metrics.height / 2 + verticalSpacing / 2
|
||||
} else {
|
||||
// Different layers - shift the later one down
|
||||
const requiredSpace = box1.y + box1.height + verticalSpacing
|
||||
if (node2.position.y < requiredSpace) {
|
||||
node2.position.y = requiredSpace
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Resolved overlap between blocks', {
|
||||
logger.debug('Resolved vertical overlap in layer', {
|
||||
layer,
|
||||
block1: node1.id,
|
||||
block2: node2.id,
|
||||
sameLayer: node1.layer === node2.layer,
|
||||
iteration,
|
||||
})
|
||||
}
|
||||
@@ -228,14 +219,15 @@ function resolveOverlaps(nodes: GraphNode[], verticalSpacing: number): void {
|
||||
}
|
||||
|
||||
if (hasOverlap) {
|
||||
logger.warn('Could not fully resolve all overlaps after max iterations', {
|
||||
logger.warn('Could not fully resolve all vertical overlaps after max iterations', {
|
||||
iterations: MAX_OVERLAP_ITERATIONS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates positions for nodes organized by layer
|
||||
* Calculates positions for nodes organized by layer.
|
||||
* Uses cumulative width-based X positioning to properly handle containers of varying widths.
|
||||
*/
|
||||
export function calculatePositions(
|
||||
layers: Map<number, GraphNode[]>,
|
||||
@@ -248,9 +240,27 @@ export function calculatePositions(
|
||||
|
||||
const layerNumbers = Array.from(layers.keys()).sort((a, b) => a - b)
|
||||
|
||||
// Calculate max width for each layer
|
||||
const layerWidths = new Map<number, number>()
|
||||
for (const layerNum of layerNumbers) {
|
||||
const nodesInLayer = layers.get(layerNum)!
|
||||
const xPosition = padding.x + layerNum * horizontalSpacing
|
||||
const maxWidth = Math.max(...nodesInLayer.map((n) => n.metrics.width))
|
||||
layerWidths.set(layerNum, maxWidth)
|
||||
}
|
||||
|
||||
// Calculate cumulative X positions for each layer based on actual widths
|
||||
const layerXPositions = new Map<number, number>()
|
||||
let cumulativeX = padding.x
|
||||
|
||||
for (const layerNum of layerNumbers) {
|
||||
layerXPositions.set(layerNum, cumulativeX)
|
||||
cumulativeX += layerWidths.get(layerNum)! + horizontalSpacing
|
||||
}
|
||||
|
||||
// Position nodes using cumulative X
|
||||
for (const layerNum of layerNumbers) {
|
||||
const nodesInLayer = layers.get(layerNum)!
|
||||
const xPosition = layerXPositions.get(layerNum)!
|
||||
|
||||
// Calculate total height for this layer
|
||||
const totalHeight = nodesInLayer.reduce(
|
||||
@@ -285,8 +295,8 @@ export function calculatePositions(
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve overlaps across all nodes
|
||||
resolveOverlaps(Array.from(layers.values()).flat(), verticalSpacing)
|
||||
// Resolve vertical overlaps within layers (X overlaps prevented by cumulative positioning)
|
||||
resolveVerticalOverlaps(Array.from(layers.values()).flat(), verticalSpacing)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
DEFAULT_HORIZONTAL_SPACING,
|
||||
DEFAULT_VERTICAL_SPACING,
|
||||
} from '@/lib/workflows/autolayout/constants'
|
||||
import { layoutContainers } from '@/lib/workflows/autolayout/containers'
|
||||
import { assignLayers, layoutBlocksCore } from '@/lib/workflows/autolayout/core'
|
||||
import type { Edge, LayoutOptions, LayoutResult } from '@/lib/workflows/autolayout/types'
|
||||
@@ -6,6 +10,7 @@ import {
|
||||
calculateSubflowDepths,
|
||||
filterLayoutEligibleBlockIds,
|
||||
getBlocksByParent,
|
||||
prepareContainerDimensions,
|
||||
} from '@/lib/workflows/autolayout/utils'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -28,6 +33,19 @@ export function applyAutoLayout(
|
||||
|
||||
const blocksCopy: Record<string, BlockState> = JSON.parse(JSON.stringify(blocks))
|
||||
|
||||
const horizontalSpacing = options.horizontalSpacing ?? DEFAULT_HORIZONTAL_SPACING
|
||||
const verticalSpacing = options.verticalSpacing ?? DEFAULT_VERTICAL_SPACING
|
||||
|
||||
// Pre-calculate container dimensions by laying out their children (bottom-up)
|
||||
// This ensures accurate widths/heights before root-level layout
|
||||
prepareContainerDimensions(
|
||||
blocksCopy,
|
||||
edges,
|
||||
layoutBlocksCore,
|
||||
horizontalSpacing,
|
||||
verticalSpacing
|
||||
)
|
||||
|
||||
const { root: rootBlockIds } = getBlocksByParent(blocksCopy)
|
||||
const layoutRootIds = filterLayoutEligibleBlockIds(rootBlockIds, blocksCopy)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getBlockMetrics,
|
||||
getBlocksByParent,
|
||||
isContainerType,
|
||||
prepareContainerDimensions,
|
||||
shouldSkipAutoLayout,
|
||||
} from '@/lib/workflows/autolayout/utils'
|
||||
import { CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
@@ -47,6 +48,16 @@ export function applyTargetedLayout(
|
||||
const changedSet = new Set(changedBlockIds)
|
||||
const blocksCopy: Record<string, BlockState> = JSON.parse(JSON.stringify(blocks))
|
||||
|
||||
// Pre-calculate container dimensions by laying out their children (bottom-up)
|
||||
// This ensures accurate widths/heights before root-level layout
|
||||
prepareContainerDimensions(
|
||||
blocksCopy,
|
||||
edges,
|
||||
layoutBlocksCore,
|
||||
horizontalSpacing,
|
||||
verticalSpacing
|
||||
)
|
||||
|
||||
const groups = getBlocksByParent(blocksCopy)
|
||||
|
||||
// Calculate subflow depths before layout to properly position blocks after subflow ends
|
||||
@@ -109,14 +120,15 @@ function layoutGroup(
|
||||
const requestedLayout = layoutEligibleChildIds.filter((id) => {
|
||||
const block = blocks[id]
|
||||
if (!block) return false
|
||||
// Never reposition containers, only update their dimensions
|
||||
if (isContainerType(block.type)) return false
|
||||
if (isContainerType(block.type)) {
|
||||
return changedSet.has(id) && isDefaultPosition(block)
|
||||
}
|
||||
return changedSet.has(id)
|
||||
})
|
||||
const missingPositions = layoutEligibleChildIds.filter((id) => {
|
||||
const block = blocks[id]
|
||||
if (!block) return false
|
||||
return !hasPosition(block)
|
||||
return !hasPosition(block) || isDefaultPosition(block)
|
||||
})
|
||||
const needsLayoutSet = new Set([...requestedLayout, ...missingPositions])
|
||||
const needsLayout = Array.from(needsLayoutSet)
|
||||
@@ -308,3 +320,13 @@ function hasPosition(block: BlockState): boolean {
|
||||
const { x, y } = block.position
|
||||
return Number.isFinite(x) && Number.isFinite(y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block is at the default/uninitialized position (0, 0).
|
||||
* New blocks typically start at this position before being laid out.
|
||||
*/
|
||||
function isDefaultPosition(block: BlockState): boolean {
|
||||
if (!block.position) return true
|
||||
const { x, y } = block.position
|
||||
return x === 0 && y === 0
|
||||
}
|
||||
|
||||
@@ -315,3 +315,126 @@ export function calculateSubflowDepths(
|
||||
|
||||
return depths
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout function type for preparing container dimensions.
|
||||
* Returns laid out nodes and bounding dimensions.
|
||||
*/
|
||||
export type LayoutFunction = (
|
||||
blocks: Record<string, BlockState>,
|
||||
edges: Edge[],
|
||||
options: {
|
||||
isContainer: boolean
|
||||
layoutOptions?: {
|
||||
horizontalSpacing?: number
|
||||
verticalSpacing?: number
|
||||
padding?: { x: number; y: number }
|
||||
alignment?: 'start' | 'center' | 'end'
|
||||
}
|
||||
subflowDepths?: Map<string, number>
|
||||
}
|
||||
) => { nodes: Map<string, GraphNode>; dimensions: { width: number; height: number } }
|
||||
|
||||
/**
|
||||
* Pre-calculates container dimensions by laying out their children.
|
||||
* Processes containers bottom-up to handle nested subflows correctly.
|
||||
* This ensures accurate width/height values before root-level layout.
|
||||
*
|
||||
* @param blocks - All blocks in the workflow (will be mutated with updated dimensions)
|
||||
* @param edges - All edges in the workflow
|
||||
* @param layoutFn - The layout function to use for calculating dimensions
|
||||
* @param horizontalSpacing - Horizontal spacing between blocks
|
||||
* @param verticalSpacing - Vertical spacing between blocks
|
||||
*/
|
||||
export function prepareContainerDimensions(
|
||||
blocks: Record<string, BlockState>,
|
||||
edges: Edge[],
|
||||
layoutFn: LayoutFunction,
|
||||
horizontalSpacing: number,
|
||||
verticalSpacing: number
|
||||
): void {
|
||||
const { children } = getBlocksByParent(blocks)
|
||||
|
||||
// Build dependency graph to process nested containers bottom-up
|
||||
const containerIds = Array.from(children.keys())
|
||||
const containerDepth = new Map<string, number>()
|
||||
|
||||
// Calculate nesting depth for each container
|
||||
for (const containerId of containerIds) {
|
||||
let depth = 0
|
||||
let currentId: string | undefined = containerId
|
||||
while (currentId) {
|
||||
const block: BlockState | undefined = blocks[currentId]
|
||||
const parentId: string | undefined = block?.data?.parentId
|
||||
currentId = parentId
|
||||
if (currentId) depth++
|
||||
}
|
||||
containerDepth.set(containerId, depth)
|
||||
}
|
||||
|
||||
// Sort containers by depth (deepest first) for bottom-up processing
|
||||
const sortedContainerIds = containerIds.sort((a, b) => {
|
||||
const depthA = containerDepth.get(a) ?? 0
|
||||
const depthB = containerDepth.get(b) ?? 0
|
||||
return depthB - depthA
|
||||
})
|
||||
|
||||
// Process each container, laying out its children to determine dimensions
|
||||
for (const containerId of sortedContainerIds) {
|
||||
const container = blocks[containerId]
|
||||
if (!container) continue
|
||||
|
||||
const childIds = children.get(containerId) ?? []
|
||||
const layoutChildIds = filterLayoutEligibleBlockIds(childIds, blocks)
|
||||
|
||||
if (layoutChildIds.length === 0) {
|
||||
// Empty container - use default dimensions
|
||||
container.data = {
|
||||
...container.data,
|
||||
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
}
|
||||
container.layout = {
|
||||
...container.layout,
|
||||
measuredWidth: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
measuredHeight: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Build subset of blocks and edges for this container's children
|
||||
const childBlocks: Record<string, BlockState> = {}
|
||||
for (const childId of layoutChildIds) {
|
||||
childBlocks[childId] = blocks[childId]
|
||||
}
|
||||
|
||||
const childEdges = edges.filter(
|
||||
(edge) => layoutChildIds.includes(edge.source) && layoutChildIds.includes(edge.target)
|
||||
)
|
||||
|
||||
// Layout children to get dimensions
|
||||
const { dimensions } = layoutFn(childBlocks, childEdges, {
|
||||
isContainer: true,
|
||||
layoutOptions: {
|
||||
horizontalSpacing: horizontalSpacing * 0.85,
|
||||
verticalSpacing,
|
||||
alignment: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
// Update container with calculated dimensions
|
||||
const calculatedWidth = Math.max(dimensions.width, CONTAINER_DIMENSIONS.DEFAULT_WIDTH)
|
||||
const calculatedHeight = Math.max(dimensions.height, CONTAINER_DIMENSIONS.DEFAULT_HEIGHT)
|
||||
|
||||
container.data = {
|
||||
...container.data,
|
||||
width: calculatedWidth,
|
||||
height: calculatedHeight,
|
||||
}
|
||||
container.layout = {
|
||||
...container.layout,
|
||||
measuredWidth: calculatedWidth,
|
||||
measuredHeight: calculatedHeight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ interface SidebarState {
|
||||
workspaceDropdownOpen: boolean
|
||||
sidebarWidth: number
|
||||
isCollapsed: boolean
|
||||
_hasHydrated: boolean
|
||||
setWorkspaceDropdownOpen: (isOpen: boolean) => void
|
||||
setSidebarWidth: (width: number) => void
|
||||
setIsCollapsed: (isCollapsed: boolean) => void
|
||||
setHasHydrated: (hasHydrated: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,6 +28,7 @@ export const useSidebarStore = create<SidebarState>()(
|
||||
workspaceDropdownOpen: false,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
isCollapsed: false,
|
||||
_hasHydrated: false,
|
||||
setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }),
|
||||
setSidebarWidth: (width) => {
|
||||
// Only enforce minimum - maximum is enforced dynamically by the resize hook
|
||||
@@ -47,17 +50,24 @@ export const useSidebarStore = create<SidebarState>()(
|
||||
document.documentElement.style.setProperty('--sidebar-width', `${currentWidth}px`)
|
||||
}
|
||||
},
|
||||
setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }),
|
||||
}),
|
||||
{
|
||||
name: 'sidebar-state',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Validate and enforce constraints after rehydration
|
||||
if (state && typeof window !== 'undefined') {
|
||||
// Use 0 width if collapsed (floating UI), otherwise use stored width
|
||||
const width = state.isCollapsed ? 0 : state.sidebarWidth
|
||||
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
|
||||
// Mark store as hydrated and apply CSS variables
|
||||
if (state) {
|
||||
state.setHasHydrated(true)
|
||||
if (typeof window !== 'undefined') {
|
||||
const width = state.isCollapsed ? 0 : state.sidebarWidth
|
||||
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
|
||||
}
|
||||
}
|
||||
},
|
||||
partialize: (state) => ({
|
||||
sidebarWidth: state.sidebarWidth,
|
||||
isCollapsed: state.isCollapsed,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
|
||||
Reference in New Issue
Block a user