Skip to content
Open
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
3 changes: 2 additions & 1 deletion apps/sim/app/api/custom-blocks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Rout
const authz = await authorizeManage(session.user.id, id)
if (authz.error) return authz.error

const { name, description, enabled, iconUrl, exposedOutputs } = parsed.data.body
const { name, description, enabled, iconUrl, inputs, exposedOutputs } = parsed.data.body
try {
await updateCustomBlock(id, {
name,
description,
enabled,
inputs,
iconUrl,
exposedOutputs,
})
Expand Down
6 changes: 5 additions & 1 deletion apps/sim/app/api/custom-blocks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ function toWire(block: CustomBlockWithInputs) {
id: block.id,
organizationId: block.organizationId,
workflowId: block.workflowId,
workflowName: block.workflowName,
workspaceName: block.workspaceName,
type: block.type,
name: block.name,
description: block.description,
Expand Down Expand Up @@ -78,7 +80,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
if (!parsed.success) return parsed.response

const userId = session.user.id
const { workspaceId, workflowId, name, description, iconUrl, exposedOutputs } = parsed.data.body
const { workspaceId, workflowId, name, description, iconUrl, inputs, exposedOutputs } =
parsed.data.body

const access = await checkWorkspaceAccess(workspaceId, userId)
if (!access.canAdmin) {
Expand Down Expand Up @@ -113,6 +116,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
name,
description,
iconUrl,
inputs,
exposedOutputs,
})
return NextResponse.json({ customBlock: toWire(block) })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useParams } from 'next/navigation'
import { buildCustomBlockConfig } from '@/blocks/custom/build-config'
import { hydrateClientCustomBlocks } from '@/blocks/custom/client-overlay'
import { getCustomBlockIcon } from '@/blocks/custom/custom-block-icon'
import { useWhitelabelSettings } from '@/ee/whitelabeling/hooks/whitelabel'
import { useCustomBlocks } from '@/hooks/queries/custom-blocks'

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

// Blocks with no uploaded icon fall back to the org's whitelabel logo, then the
// default glyph. All blocks share the org, so read it off the first row.
const { data: whitelabel } = useWhitelabelSettings(data?.[0]?.organizationId)
const fallbackIconUrl = whitelabel?.logoUrl ?? null

useEffect(() => {
hydrateClientCustomBlocks(
// Only enabled blocks are resolvable/executable server-side, so the client
// overlay (toolbar, canvas, palette) must exclude disabled ones too — else
// the block is offered but every run fails.
(data ?? [])
.filter((block) => block.enabled)
.map((block) =>
buildCustomBlockConfig(
{
type: block.type,
name: block.name,
description: block.description,
workflowId: block.workflowId,
exposedOutputs: block.exposedOutputs,
},
block.inputFields,
{
icon: getCustomBlockIcon(block.iconUrl),
bgColor: block.iconUrl ? 'transparent' : undefined,
}
)
// Disabled blocks stay resolvable (so a still-placed instance renders on the
// canvas and survives serialization instead of vanishing) but are hidden from
// the palette so no new instance can be placed; a run fails loudly server-side.
(data ?? []).map((block) => {
const effectiveIcon = block.iconUrl || fallbackIconUrl
return buildCustomBlockConfig(
{
type: block.type,
name: block.name,
description: block.description,
workflowId: block.workflowId,
exposedOutputs: block.exposedOutputs,
},
block.inputFields,
{
icon: getCustomBlockIcon(block.iconUrl, fallbackIconUrl),
bgColor: effectiveIcon ? 'transparent' : undefined,
hideFromToolbar: !block.enabled,
}
)
})
)
}, [data])
}, [data, fallbackIconUrl])

return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ const WorkflowMcpServers = dynamic(() =>
const AccessControl = dynamic(() =>
import('@/ee/access-control/components/access-control').then((m) => m.AccessControl)
)
const CustomBlocks = dynamic(() =>
import('@/ee/custom-blocks/components/custom-blocks').then((m) => m.CustomBlocks)
)
const AuditLogs = dynamic(() =>
import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs)
)
Expand Down Expand Up @@ -122,6 +125,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
{effectiveSection === 'general' && <General />}
{effectiveSection === 'secrets' && <Secrets />}
{effectiveSection === 'access-control' && <AccessControl />}
{effectiveSection === 'custom-blocks' && <CustomBlocks />}
{effectiveSection === 'audit-logs' && <AuditLogs />}
{effectiveSection === 'apikeys' && <ApiKeys />}
{isBillingEnabled && effectiveSection === 'billing' && <Billing />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,23 @@ export function useProfilePictureUpload({
onUploadRef.current?.(null)
}, [])

/**
* Restore the preview to the stored image (`currentImage`) — for a form discard
* that should revert an unsaved pick/removal without clearing the saved image.
* Unlike {@link handleRemove}, this does not emit `onUpload(null)`.
*/
const reset = useCallback(() => {
if (previewRef.current) {
URL.revokeObjectURL(previewRef.current)
previewRef.current = null
}
setPreviewUrl(currentImageRef.current || null)
setFileName(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}, [])

useEffect(() => {
return () => {
if (previewRef.current) {
Expand All @@ -173,6 +190,7 @@ export function useProfilePictureUpload({
handleFileChange,
handleFileDrop,
handleRemove,
reset,
isUploading,
}
}
12 changes: 12 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type SettingsSection =
| 'general'
| 'secrets'
| 'access-control'
| 'custom-blocks'
| 'audit-logs'
| 'apikeys'
| 'byok'
Expand Down Expand Up @@ -78,6 +79,7 @@ export interface NavigationItem {

const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED'))
const isCustomBlocksEnabled = isTruthy(getEnv('NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED'))
const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED'))
const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED'))
const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED'))
Expand Down Expand Up @@ -264,6 +266,16 @@ export const allNavigationItems: NavigationItem[] = [
selfHostedOverride: isWhitelabelingEnabled,
docsLink: 'https://docs.sim.ai/platform/enterprise/whitelabeling',
},
{
id: 'custom-blocks',
label: 'Custom blocks',
description: 'Publish workflows as reusable blocks for your organization.',
icon: HexSimple,
section: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
selfHostedOverride: isCustomBlocksEnabled,
},
{
id: 'admin',
label: 'Admin',
Expand Down
Loading
Loading