Skip to content

Commit fe7d043

Browse files
feat(custom-block): move management to enterprise settings; derive inputs live from deployed start (#5453)
* feat(custom-block): move management to enterprise settings; derive inputs live from deployed start * fix(custom-block): key custom-blocks query by workspaceId (react-query audit) * fix(custom-block): restore icon on discard instead of clearing the saved image * fix(custom-block): track create-flow dirty state and reset selections on discard * fix(custom-block): merge placeholders for id-less start fields; compare inputs by authored data only * improvement(custom-block): dedup input-mapping helper, reuse SettingsResourceRow + shared reserved-param set * fix(custom-block): keep disabled blocks resolvable so placed instances fail loudly instead of vanishing * fix(custom-block): derive fields for disabled blocks so edits keep placeholders; reseed edit form after async load * fix(custom-block): scope publish workspace picker to the current org
1 parent c34a3bc commit fe7d043

31 files changed

Lines changed: 18038 additions & 552 deletions

File tree

apps/sim/app/api/custom-blocks/[id]/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,13 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Rout
5757
const authz = await authorizeManage(session.user.id, id)
5858
if (authz.error) return authz.error
5959

60-
const { name, description, enabled, iconUrl, exposedOutputs } = parsed.data.body
60+
const { name, description, enabled, iconUrl, inputs, exposedOutputs } = parsed.data.body
6161
try {
6262
await updateCustomBlock(id, {
6363
name,
6464
description,
6565
enabled,
66+
inputs,
6667
iconUrl,
6768
exposedOutputs,
6869
})

apps/sim/app/api/custom-blocks/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ function toWire(block: CustomBlockWithInputs) {
2727
id: block.id,
2828
organizationId: block.organizationId,
2929
workflowId: block.workflowId,
30+
workflowName: block.workflowName,
31+
workspaceName: block.workspaceName,
3032
type: block.type,
3133
name: block.name,
3234
description: block.description,
@@ -78,7 +80,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
7880
if (!parsed.success) return parsed.response
7981

8082
const userId = session.user.id
81-
const { workspaceId, workflowId, name, description, iconUrl, exposedOutputs } = parsed.data.body
83+
const { workspaceId, workflowId, name, description, iconUrl, inputs, exposedOutputs } =
84+
parsed.data.body
8285

8386
const access = await checkWorkspaceAccess(workspaceId, userId)
8487
if (!access.canAdmin) {
@@ -113,6 +116,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
113116
name,
114117
description,
115118
iconUrl,
119+
inputs,
116120
exposedOutputs,
117121
})
118122
return NextResponse.json({ customBlock: toWire(block) })

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

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useParams } from 'next/navigation'
55
import { buildCustomBlockConfig } from '@/blocks/custom/build-config'
66
import { hydrateClientCustomBlocks } from '@/blocks/custom/client-overlay'
77
import { getCustomBlockIcon } from '@/blocks/custom/custom-block-icon'
8+
import { useWhitelabelSettings } from '@/ee/whitelabeling/hooks/whitelabel'
89
import { useCustomBlocks } from '@/hooks/queries/custom-blocks'
910

1011
/**
@@ -19,31 +20,36 @@ export function CustomBlocksLoader() {
1920
const workspaceId = params?.workspaceId as string | undefined
2021
const { data } = useCustomBlocks(workspaceId)
2122

23+
// Blocks with no uploaded icon fall back to the org's whitelabel logo, then the
24+
// default glyph. All blocks share the org, so read it off the first row.
25+
const { data: whitelabel } = useWhitelabelSettings(data?.[0]?.organizationId)
26+
const fallbackIconUrl = whitelabel?.logoUrl ?? null
27+
2228
useEffect(() => {
2329
hydrateClientCustomBlocks(
24-
// Only enabled blocks are resolvable/executable server-side, so the client
25-
// overlay (toolbar, canvas, palette) must exclude disabled ones too — else
26-
// the block is offered but every run fails.
27-
(data ?? [])
28-
.filter((block) => block.enabled)
29-
.map((block) =>
30-
buildCustomBlockConfig(
31-
{
32-
type: block.type,
33-
name: block.name,
34-
description: block.description,
35-
workflowId: block.workflowId,
36-
exposedOutputs: block.exposedOutputs,
37-
},
38-
block.inputFields,
39-
{
40-
icon: getCustomBlockIcon(block.iconUrl),
41-
bgColor: block.iconUrl ? 'transparent' : undefined,
42-
}
43-
)
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+
}
4449
)
50+
})
4551
)
46-
}, [data])
52+
}, [data, fallbackIconUrl])
4753

4854
return null
4955
}

apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const WorkflowMcpServers = dynamic(() =>
7070
const AccessControl = dynamic(() =>
7171
import('@/ee/access-control/components/access-control').then((m) => m.AccessControl)
7272
)
73+
const CustomBlocks = dynamic(() =>
74+
import('@/ee/custom-blocks/components/custom-blocks').then((m) => m.CustomBlocks)
75+
)
7376
const AuditLogs = dynamic(() =>
7477
import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs)
7578
)
@@ -122,6 +125,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
122125
{effectiveSection === 'general' && <General />}
123126
{effectiveSection === 'secrets' && <Secrets />}
124127
{effectiveSection === 'access-control' && <AccessControl />}
128+
{effectiveSection === 'custom-blocks' && <CustomBlocks />}
125129
{effectiveSection === 'audit-logs' && <AuditLogs />}
126130
{effectiveSection === 'apikeys' && <ApiKeys />}
127131
{isBillingEnabled && effectiveSection === 'billing' && <Billing />}

apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,23 @@ export function useProfilePictureUpload({
157157
onUploadRef.current?.(null)
158158
}, [])
159159

160+
/**
161+
* Restore the preview to the stored image (`currentImage`) — for a form discard
162+
* that should revert an unsaved pick/removal without clearing the saved image.
163+
* Unlike {@link handleRemove}, this does not emit `onUpload(null)`.
164+
*/
165+
const reset = useCallback(() => {
166+
if (previewRef.current) {
167+
URL.revokeObjectURL(previewRef.current)
168+
previewRef.current = null
169+
}
170+
setPreviewUrl(currentImageRef.current || null)
171+
setFileName(null)
172+
if (fileInputRef.current) {
173+
fileInputRef.current.value = ''
174+
}
175+
}, [])
176+
160177
useEffect(() => {
161178
return () => {
162179
if (previewRef.current) {
@@ -173,6 +190,7 @@ export function useProfilePictureUpload({
173190
handleFileChange,
174191
handleFileDrop,
175192
handleRemove,
193+
reset,
176194
isUploading,
177195
}
178196
}

apps/sim/app/workspace/[workspaceId]/settings/navigation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type SettingsSection =
2525
| 'general'
2626
| 'secrets'
2727
| 'access-control'
28+
| 'custom-blocks'
2829
| 'audit-logs'
2930
| 'apikeys'
3031
| 'byok'
@@ -78,6 +79,7 @@ export interface NavigationItem {
7879

7980
const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
8081
const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED'))
82+
const isCustomBlocksEnabled = isTruthy(getEnv('NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED'))
8183
const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED'))
8284
const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED'))
8385
const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED'))
@@ -264,6 +266,16 @@ export const allNavigationItems: NavigationItem[] = [
264266
selfHostedOverride: isWhitelabelingEnabled,
265267
docsLink: 'https://docs.sim.ai/platform/enterprise/whitelabeling',
266268
},
269+
{
270+
id: 'custom-blocks',
271+
label: 'Custom blocks',
272+
description: 'Publish workflows as reusable blocks for your organization.',
273+
icon: HexSimple,
274+
section: 'enterprise',
275+
requiresHosted: true,
276+
requiresEnterprise: true,
277+
selfHostedOverride: isCustomBlocksEnabled,
278+
},
267279
{
268280
id: 'admin',
269281
label: 'Admin',

0 commit comments

Comments
 (0)