Skip to content

Commit 70b8ed1

Browse files
fix(custom-block): keep disabled blocks resolvable so placed instances fail loudly instead of vanishing
1 parent 75e0c89 commit 70b8ed1

6 files changed

Lines changed: 58 additions & 35 deletions

File tree

apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,27 @@ export function CustomBlocksLoader() {
2727

2828
useEffect(() => {
2929
hydrateClientCustomBlocks(
30-
// Only enabled blocks are resolvable/executable server-side, so the client
31-
// overlay (toolbar, canvas, palette) must exclude disabled ones too — else
32-
// the block is offered but every run fails.
33-
(data ?? [])
34-
.filter((block) => block.enabled)
35-
.map((block) => {
36-
const effectiveIcon = block.iconUrl || fallbackIconUrl
37-
return buildCustomBlockConfig(
38-
{
39-
type: block.type,
40-
name: block.name,
41-
description: block.description,
42-
workflowId: block.workflowId,
43-
exposedOutputs: block.exposedOutputs,
44-
},
45-
block.inputFields,
46-
{
47-
icon: getCustomBlockIcon(block.iconUrl, fallbackIconUrl),
48-
bgColor: effectiveIcon ? 'transparent' : undefined,
49-
}
50-
)
51-
})
30+
// Disabled blocks stay resolvable (so a still-placed instance renders on the
31+
// canvas and survives serialization instead of vanishing) but are hidden from
32+
// the palette so no new instance can be placed; a run fails loudly server-side.
33+
(data ?? []).map((block) => {
34+
const effectiveIcon = block.iconUrl || fallbackIconUrl
35+
return buildCustomBlockConfig(
36+
{
37+
type: block.type,
38+
name: block.name,
39+
description: block.description,
40+
workflowId: block.workflowId,
41+
exposedOutputs: block.exposedOutputs,
42+
},
43+
block.inputFields,
44+
{
45+
icon: getCustomBlockIcon(block.iconUrl, fallbackIconUrl),
46+
bgColor: effectiveIcon ? 'transparent' : undefined,
47+
hideFromToolbar: !block.enabled,
48+
}
49+
)
50+
})
5251
)
5352
}, [data, fallbackIconUrl])
5453

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,9 +459,9 @@ export const Toolbar = memo(
459459
const allBlocks = getBlocks()
460460
const allTools = getTools()
461461

462-
// Published custom blocks are their own section. Exclude disabled blocks (the
463-
// server overlay drops them, so placing one would fail at run) and the block
464-
// bound to the CURRENT workflow — adding a workflow's own block would recurse.
462+
// Published custom blocks are their own section. Exclude disabled blocks (still
463+
// resolvable so placed instances survive, but not offered for new placement) and
464+
// the block bound to the CURRENT workflow — adding a workflow's own block recurses.
465465
const allCustomBlocks = useMemo(() => {
466466
if (!customBlocksData?.length) return []
467467
return customBlocksData

apps/sim/blocks/custom/build-config.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ describe('buildCustomBlockConfig', () => {
5454
expect(config.tools.config?.tool({})).toBe('workflow_executor')
5555
})
5656

57+
it('hides a disabled block from the toolbar while keeping it resolvable', () => {
58+
expect(buildCustomBlockConfig(row, fields, { icon }).hideFromToolbar).toBeUndefined()
59+
expect(
60+
buildCustomBlockConfig(row, fields, { icon, hideFromToolbar: true }).hideFromToolbar
61+
).toBe(true)
62+
})
63+
5764
it('bakes the bound workflowId as a hidden sub-block', () => {
5865
const config = buildCustomBlockConfig(row, fields, { icon })
5966
const wf = findSub(config, 'workflowId')

apps/sim/blocks/custom/build-config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function subBlockTypeForField(fieldType: string): SubBlockType {
9898
export function buildCustomBlockConfig(
9999
row: CustomBlockRow,
100100
inputFields: WorkflowInputField[],
101-
opts: { icon: BlockIcon; bgColor?: string }
101+
opts: { icon: BlockIcon; bgColor?: string; hideFromToolbar?: boolean }
102102
): BlockConfig {
103103
const fieldSubBlocks: SubBlockConfig[] = inputFields.map((field) => {
104104
const type = subBlockTypeForField(field.type)
@@ -125,6 +125,11 @@ export function buildCustomBlockConfig(
125125
'workflow is baked in — no workflow id or input mapping to configure.',
126126
bgColor: opts.bgColor ?? CUSTOM_BLOCK_TILE_COLOR,
127127
icon: opts.icon,
128+
// A disabled block stays resolvable (so a still-placed instance survives
129+
// serialization and fails loudly at run via `getCustomBlockAuthority`, instead
130+
// of silently vanishing from the graph) but is hidden from the palette so no
131+
// new instance can be placed.
132+
hideFromToolbar: opts.hideFromToolbar,
128133
subBlocks: [
129134
{
130135
id: 'workflowId',

apps/sim/blocks/custom/server-overlay.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { registerBlockOverlayResolver } from '@/blocks/custom/overlay'
55
import type { BlockConfig, BlockIcon } from '@/blocks/types'
66

77
/** A row for the overlay, optionally carrying live-derived Start input fields. */
8-
type CustomBlockOverlayRow = CustomBlockRow & { inputFields?: WorkflowInputField[] }
8+
type CustomBlockOverlayRow = CustomBlockRow & {
9+
inputFields?: WorkflowInputField[]
10+
/** When `false`, the block resolves but is hidden from the palette (disabled). */
11+
enabled?: boolean
12+
}
913

1014
/**
1115
* Server-side custom-block overlay. Resolves `custom_block_*` types during
@@ -43,7 +47,10 @@ export function withCustomBlockOverlay<T>(
4347
for (const row of rows) {
4448
map.set(
4549
row.type,
46-
buildCustomBlockConfig(row, row.inputFields ?? [], { icon: PLACEHOLDER_ICON })
50+
buildCustomBlockConfig(row, row.inputFields ?? [], {
51+
icon: PLACEHOLDER_ICON,
52+
hideFromToolbar: row.enabled === false,
53+
})
4754
)
4855
}
4956
return store.run(map, fn)

apps/sim/lib/workflows/custom-blocks/operations.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,23 +86,28 @@ function applyInputPlaceholders(
8686
}
8787

8888
/**
89-
* The org's custom blocks as bare `CustomBlockRow`s for the server overlay
90-
* (`withCustomBlockOverlay`). Only enabled rows — a disabled block must not resolve
91-
* for execution. No input fields: the server's `inputMapping` is schema-agnostic
92-
* and the handler's remap filters every value against the child's live deployed
93-
* Start, so execution never needs to load each block's deployment here.
89+
* The org's custom blocks for the server overlay (`withCustomBlockOverlay`).
90+
* Includes DISABLED rows (carrying `enabled`) so a still-placed disabled block
91+
* stays resolvable — it survives serialization and fails loudly at run via
92+
* `getCustomBlockAuthority` instead of being silently dropped from the graph; the
93+
* overlay marks it `hideFromToolbar` so no new instance can be placed. No input
94+
* fields: the server's `inputMapping` is schema-agnostic and the handler's remap
95+
* filters every value against the child's live deployed Start.
9496
*/
95-
export async function getCustomBlockRowsForOrg(organizationId: string): Promise<CustomBlockRow[]> {
97+
export async function getCustomBlockRowsForOrg(
98+
organizationId: string
99+
): Promise<Array<CustomBlockRow & { enabled: boolean }>> {
96100
const rows = await db
97101
.select({
98102
type: customBlock.type,
99103
name: customBlock.name,
100104
description: customBlock.description,
101105
workflowId: customBlock.workflowId,
102106
outputs: customBlock.outputs,
107+
enabled: customBlock.enabled,
103108
})
104109
.from(customBlock)
105-
.where(and(eq(customBlock.organizationId, organizationId), eq(customBlock.enabled, true)))
110+
.where(eq(customBlock.organizationId, organizationId))
106111

107112
return rows.map(({ outputs, ...r }) => ({ ...r, exposedOutputs: outputs ?? [] }))
108113
}

0 commit comments

Comments
 (0)