Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ fi
# Generate schema and run database migrations
echo "🗃️ Running database schema generation and migrations..."
echo "Generating schema..."
cd apps/sim
bunx drizzle-kit generate
cd packages/db
bun run db:generate
cd ../..

echo "Waiting for database to be ready..."
Expand All @@ -105,8 +105,8 @@ echo "Waiting for database to be ready..."
while [ $timeout -gt 0 ]; do
if PGPASSWORD=postgres psql -h db -U postgres -c '\q' 2>/dev/null; then
echo "Database is ready!"
cd apps/sim
DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio bunx drizzle-kit push
cd packages/db
DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio bun run db:push
cd ../..
break
fi
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/sim-commands.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
alias sim-start="cd /workspace && bun run dev:full"
alias sim-app="cd /workspace && bun run dev"
alias sim-sockets="cd /workspace && bun run dev:sockets"
alias sim-migrate="cd /workspace/apps/sim && bunx drizzle-kit push"
alias sim-generate="cd /workspace/apps/sim && bunx drizzle-kit generate"
alias sim-migrate="cd /workspace/packages/db && bun run db:push"
alias sim-generate="cd /workspace/packages/db && bun run db:generate"
alias sim-rebuild="cd /workspace && bun run build && bun run start"
alias docs-dev="cd /workspace/apps/docs && bun run dev"

Expand Down
5 changes: 5 additions & 0 deletions apps/docs/components/workflow-preview/block-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use client'

import { useMemo } from 'react'
import { CANVAS_Z_INDEX_MODE } from '@sim/workflow-renderer'
import { type NodeTypes, ReactFlow, ReactFlowProvider } from '@xyflow/react'
import { domAnimation, LazyMotion } from 'framer-motion'
import '@xyflow/react/dist/style.css'
import { BLOCK_DISPLAY_WORKFLOWS } from '@/components/workflow-preview/block-display-workflows'
import { DocsBlockNode } from '@/components/workflow-preview/docs-block-node'
import { usePreviewColorMode } from '@/components/workflow-preview/use-preview-color-mode'
import { toReactFlowElements } from '@/components/workflow-preview/workflow-data'

/** The hero mounts the same node type the canvas uses, so it can never drift. */
Expand All @@ -28,6 +30,7 @@ interface BlockPreviewProps {
* `block-display-workflows.ts`.
*/
export function BlockPreview({ type }: BlockPreviewProps) {
const colorMode = usePreviewColorMode()
const workflow = BLOCK_DISPLAY_WORKFLOWS[type]

const elements = useMemo(() => (workflow ? toReactFlowElements(workflow) : null), [workflow])
Expand All @@ -42,6 +45,8 @@ export function BlockPreview({ type }: BlockPreviewProps) {
<LazyMotion features={domAnimation}>
<ReactFlowProvider>
<ReactFlow
colorMode={colorMode}
zIndexMode={CANVAS_Z_INDEX_MODE}
nodes={elements.nodes}
edges={elements.edges}
nodeTypes={NODE_TYPES}
Expand Down
17 changes: 17 additions & 0 deletions apps/docs/components/workflow-preview/use-preview-color-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'

import type { ColorMode } from '@xyflow/react'
import { useTheme } from 'next-themes'

/**
* Resolves the React Flow `colorMode` from the docs theme so the canvas
* wrapper's color-mode class (and the `--xy-*` palette it selects) follows
* dark mode instead of React Flow's default `light`.
*/
export function usePreviewColorMode(): ColorMode {
const { resolvedTheme } = useTheme()
// Before next-themes mounts, resolvedTheme is undefined; 'system' lets React
// Flow follow the OS preference instead of flashing a light-classed frame.
if (resolvedTheme === undefined) return 'system'
return resolvedTheme === 'dark' ? 'dark' : 'light'
}
5 changes: 5 additions & 0 deletions apps/docs/components/workflow-preview/workflow-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Expand, X } from '@sim/emcn/icons'
import { CANVAS_Z_INDEX_MODE } from '@sim/workflow-renderer'
import {
applyEdgeChanges,
applyNodeChanges,
Expand All @@ -20,6 +21,7 @@ import { BLOCK_DISPLAY_WORKFLOWS } from '@/components/workflow-preview/block-dis
import { BlockInspector } from '@/components/workflow-preview/block-inspector'
import { DocsBlockNode } from '@/components/workflow-preview/docs-block-node'
import { DocsContainerNode } from '@/components/workflow-preview/docs-container-node'
import { usePreviewColorMode } from '@/components/workflow-preview/use-preview-color-mode'
import {
EASE_OUT,
type PreviewBlock,
Expand Down Expand Up @@ -177,6 +179,7 @@ function PreviewFlow({
[workflow, animate, highlightBlock, highlightEdge, selectedBlock]
)

const colorMode = usePreviewColorMode()
const [nodes, setNodes] = useState<PreviewNode[]>(initialNodes)
const [edges, setEdges] = useState<PreviewFlowEdge[]>(initialEdges)

Expand Down Expand Up @@ -206,6 +209,8 @@ function PreviewFlow({

return (
<ReactFlow<PreviewNode, PreviewFlowEdge>
colorMode={colorMode}
zIndexMode={CANVAS_Z_INDEX_MODE}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ export function resolveCalLink(configuredLink?: string): URL {
return url
}

const calLinkUrl = resolveCalLink(process.env.NEXT_PUBLIC_CAL_LINK)
/**
* Resolved at module scope on an eagerly-imported path, so a malformed
* NEXT_PUBLIC_CAL_LINK degrades to the default link instead of taking the
* whole /demo page down.
*/
const calLinkUrl = (() => {
try {
return resolveCalLink(process.env.NEXT_PUBLIC_CAL_LINK)
} catch {
return resolveCalLink(undefined)
}
})()

/** Exact origin used for iframe navigation, preconnect, and postMessage validation. */
export const CAL_ORIGIN = calLinkUrl.origin
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,8 @@ input[type="search"]::-ms-clear {
border-radius: 8px !important;
}

.react-flow__node[data-parent-node-id] .react-flow__handle {
/* React Flow v12 no longer emits data-parent-node-id; the app stamps
.subflow-child-node (SUBFLOW_CHILD_NODE_CLASS) on nested nodes instead. */
.react-flow__node.subflow-child-node .react-flow__handle {
Comment thread
waleedlatif1 marked this conversation as resolved.
z-index: 30;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import type { BlockState } from '@/stores/workflows/workflow/types'

export const SUBFLOW_DROP_TARGET_CLASS = 'subflow-node-drop-target'

/**
* Marks canvas nodes nested inside a subflow container. React Flow v11 emitted
* `data-parent-node-id` for this; v12 emits no parent attribute, so the app
* stamps its own class for the handle z-lift in `globals.css`.
*/
export const SUBFLOW_CHILD_NODE_CLASS = 'subflow-child-node'

export function getNodeDataDimension(
node: Pick<Node, 'data'>,
dimension: 'width' | 'height',
Expand Down Expand Up @@ -55,11 +62,10 @@ function reconcileById<T extends { id: string }>(
}

/**
* Subset comparison, deliberately asymmetric: React Flow writes `width`,
* `height`, `positionAbsolute` and `dragging` onto the node objects it owns, so
* a symmetric `isEqual` against a freshly derived node would never match and no
* node would ever be reused. Only the keys the derivation itself produces are
* compared.
* Subset comparison, deliberately asymmetric: React Flow writes `measured` and
* `dragging` onto the node objects it owns, so a symmetric `isEqual` against a
* freshly derived node would never match and no node would ever be reused. Only
* the keys the derivation itself produces are compared.
*/
function containsDerivedValues<T extends object>(current: T, derived: T): boolean {
for (const key of Object.keys(derived) as (keyof T)[]) {
Expand All @@ -68,12 +74,28 @@ function containsDerivedValues<T extends object>(current: T, derived: T): boolea
return true
}

/** Reuses unchanged React Flow node objects while carrying local selection forward. */
export function reconcileCanvasNodes(currentNodes: Node[], derivedNodes: Node[]): Node[] {
/**
* Reuses unchanged React Flow node objects while carrying local selection and
* measured dimensions forward. `measured` must survive re-derivation: React
* Flow resets a node's cached handle bounds and re-measures whenever a node
* object arrives without it, which snaps connected edges for a frame.
*
* @param selectedIds - When provided, overrides selection instead of carrying
* it forward (e.g. the pending selection applied after paste/duplicate)
*/
export function reconcileCanvasNodes(
currentNodes: Node[],
derivedNodes: Node[],
selectedIds?: ReadonlySet<string>
): Node[] {
return reconcileById(
currentNodes,
derivedNodes,
(derivedNode, currentNode) => ({ ...derivedNode, selected: currentNode?.selected ?? false }),
(derivedNode, currentNode) => ({
...derivedNode,
measured: currentNode?.measured,
selected: selectedIds ? selectedIds.has(derivedNode.id) : (currentNode?.selected ?? false),
}),
containsDerivedValues
)
}
Expand Down
39 changes: 24 additions & 15 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { SubflowNodeData } from '@sim/workflow-renderer'
import {
BLOCK_DIMENSIONS,
BLOCK_Z_BASE,
CANVAS_Z_INDEX_MODE,
CONNECTION_PICKER_Z,
CONTAINER_CHILD_Z_BASE,
CONTAINER_DIMENSIONS,
Expand Down Expand Up @@ -102,6 +103,7 @@ import {
reconcileCanvasEdges,
reconcileCanvasNodes,
resolveSelectionConflicts,
SUBFLOW_CHILD_NODE_CLASS,
SUBFLOW_DROP_TARGET_CLASS,
shouldHighlightContainerDropTarget,
validateTriggerPaste,
Expand Down Expand Up @@ -136,6 +138,7 @@ import {
isFolderOrAncestorLocked,
} from '@/hooks/queries/utils/folder-tree'
import { useUpdateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows'
import { useCanvasColorMode } from '@/hooks/use-canvas-color-mode'
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useOAuthReturnForWorkflow } from '@/hooks/use-oauth-return'
Expand Down Expand Up @@ -342,6 +345,7 @@ const WorkflowContent = React.memo(

const params = useParams()
const router = useRouter()
const colorMode = useCanvasColorMode()
const reactFlowInstance = useReactFlow()
const { screenToFlowPosition, getNodes, setNodes } = reactFlowInstance
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, {
Expand Down Expand Up @@ -2869,6 +2873,7 @@ const WorkflowContent = React.memo(
type: 'subflowNode',
position: block.position,
parentId: block.data?.parentId,
className: block.data?.parentId ? SUBFLOW_CHILD_NODE_CLASS : undefined,
extent: block.data?.extent || undefined,
dragHandle: '.workflow-drag-handle',
draggable: !workflowReadOnly && !isBlockProtected(block.id, blocks),
Expand Down Expand Up @@ -2907,20 +2912,21 @@ const WorkflowContent = React.memo(
// level as a subflow container and below the edge band. A card inside a
// container starts higher still, so it clears the parent's interactive
// body area (which needs pointer-events for click-to-select).
const cardZIndex = block.data?.parentId ? CONTAINER_CHILD_Z_BASE : BLOCK_Z_BASE
const parentId = block.data?.parentId as string | undefined
const cardZIndex = parentId ? CONTAINER_CHILD_Z_BASE : BLOCK_Z_BASE

// Create stable node object - React Flow will handle shallow comparison
nodeArray.push({
id: block.id,
type: nodeType,
position,
parentId: block.data?.parentId,
parentId,
className: parentId ? SUBFLOW_CHILD_NODE_CLASS : undefined,
dragHandle,
draggable: !workflowReadOnly && !isBlockProtected(block.id, blocks),
zIndex: cardZIndex,
extent: (() => {
// Clamp children to subflow body (exclude header)
const parentId = block.data?.parentId as string | undefined
if (!parentId) return block.data?.extent || undefined

// Constrain the top and left to the container's own gutter, the same
Expand Down Expand Up @@ -2949,11 +2955,13 @@ const WorkflowContent = React.memo(
onSetErrorOutputEnabled: collaborativeSetBlockErrorEnabled,
onRemoveEdges: collaborativeBatchRemoveEdges,
},
// Include dynamic dimensions for container resizing calculations (must match rendered size)
// Both note and workflow blocks calculate dimensions deterministically via useBlockDimensions
// Use estimated dimensions for blocks without measured height to ensure selection bounds are correct
width: getRegularBlockWidth(block.type),
height: block.height
// Seed dimensions so selection bounds and container-resize math are
// valid before the first measurement. These must stay `initial*`: in
// React Flow v12 top-level `width`/`height` become fixed inline
// styles that clamp the node, while `initialWidth`/`initialHeight`
// only stand in until the rendered content is measured.
initialWidth: getRegularBlockWidth(block.type),
initialHeight: block.height
? block.type === 'note'
? block.height
: Math.max(block.height, BLOCK_DIMENSIONS.MIN_HEIGHT)
Expand Down Expand Up @@ -3003,12 +3011,12 @@ const WorkflowContent = React.memo(
clearPendingSelection()

// Apply pending selection and resolve parent-child conflicts
const withSelection = derivedNodes.map((node) => ({
...node,
selected: pendingSet.has(node.id),
}))
const resolved = resolveSelectionConflicts(withSelection, blocks)
setDisplayNodes(resolved)
setDisplayNodes((currentNodes) =>
resolveSelectionConflicts(
reconcileCanvasNodes(currentNodes, derivedNodes, pendingSet),
blocks
)
)
return
}

Expand Down Expand Up @@ -5120,6 +5128,8 @@ const WorkflowContent = React.memo(
{isWorkflowReady && (
<>
<ReactFlow
colorMode={colorMode}
zIndexMode={CANVAS_Z_INDEX_MODE}
nodes={nodesForRender}
edges={edgesForRender}
onNodesChange={onNodesChange}
Expand Down Expand Up @@ -5197,7 +5207,6 @@ const WorkflowContent = React.memo(
draggable={false}
noWheelClassName='allow-scroll'
edgesFocusable={!embedded}
edgesReconnectable={!embedded && effectivePermissions.canEdit}
className={`workflow-container h-full bg-[var(--bg)] transition-opacity duration-150 ${reactFlowStyles} ${canvasOpacityClass} ${isHandMode ? 'canvas-mode-hand' : 'canvas-mode-cursor'}`}
onNodeDrag={effectivePermissions.canEdit ? onNodeDrag : undefined}
onNodeDragStop={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { createLogger } from '@sim/logger'
import {
BLOCK_DIMENSIONS,
BLOCK_Z_BASE,
CANVAS_Z_INDEX_MODE,
CONTAINER_CHILD_Z_BASE,
CONTAINER_DIMENSIONS,
EDGE_Z_BASE,
Expand All @@ -27,10 +28,14 @@ import {
} from '@sim/workflow-renderer'
import { normalizeWorkflowEdgeHandles } from '@sim/workflow-types/workflow'
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
import {
estimateBlockDimensions,
SUBFLOW_CHILD_NODE_CLASS,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
import { PreviewBlock } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block'
import { PreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow'
import { useWorkflowMap } from '@/hooks/queries/workflows'
import { useCanvasColorMode } from '@/hooks/use-canvas-color-mode'
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'

const logger = createLogger('PreviewWorkflow')
Expand Down Expand Up @@ -251,6 +256,7 @@ export function PreviewWorkflow({
// placeholder map must not mislabel valid workflows as deleted.
const workflowLabelsReady = isWorkflowMapLoaded && !isWorkflowMapPlaceholderData
const containerRef = useRef<HTMLDivElement>(null)
const colorMode = useCanvasColorMode()
const nodeTypes = previewNodeTypes
const isValidWorkflowState = workflowState?.blocks && workflowState.edges

Expand Down Expand Up @@ -429,6 +435,7 @@ export function PreviewWorkflow({
type: 'subflowNode',
position: block.position,
parentId,
className: parentId ? SUBFLOW_CHILD_NODE_CLASS : undefined,
extent: block.data?.extent || undefined,
draggable: false,
zIndex: nestingDepth,
Expand Down Expand Up @@ -472,6 +479,7 @@ export function PreviewWorkflow({
type: nodeType,
position: block.position,
parentId,
className: parentId ? SUBFLOW_CHILD_NODE_CLASS : undefined,
extent: block.data?.extent || undefined,
draggable: false,
zIndex: parentId ? CONTAINER_CHILD_Z_BASE : BLOCK_Z_BASE,
Expand Down Expand Up @@ -654,6 +662,8 @@ export function PreviewWorkflow({
.preview-mode.interactive-nodes .react-flow__node * { cursor: pointer !important; }
`}</style>
<ReactFlow
colorMode={colorMode}
zIndexMode={CANVAS_Z_INDEX_MODE}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/components/emails/boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ describe('every email goes through the shared layer', () => {

it('no sender reaches for a split React Email package', () => {
// Substring, not an import-statement match — senders here use `await import()` too.
// `react-email` re-exports `render`, so importing it directly from the
// meta-package must be caught the same as the split packages.
const offenders = senderFiles.filter((f) =>
/@react-email\/(components|render)/.test(readFileSync(f, 'utf8'))
/@react-email\/(components|render)|['"]react-email['"]/.test(readFileSync(f, 'utf8'))
)
expect(offenders.map(rel)).toEqual([])
})
Expand Down
Loading
Loading