Files
simstudioai--sim/apps/sim/ee/custom-blocks/components/custom-blocks.tsx
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

133 lines
4.8 KiB
TypeScript

'use client'
import { useMemo, useState } from 'react'
import { ChipTag } from '@sim/emcn'
import { ArrowRight, Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { getCustomBlockIcon } from '@/blocks/custom/custom-block-icon'
import { CustomBlockDetail } from '@/ee/custom-blocks/components/custom-block-detail'
import { useWhitelabelSettings } from '@/ee/whitelabeling/hooks/whitelabel'
import { useCanPublishCustomBlock, useCustomBlocks } from '@/hooks/queries/custom-blocks'
import { useWorkspacesQuery } from '@/hooks/queries/workspace'
export function CustomBlocks() {
const params = useParams()
const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : undefined
const { data: canManage = false, isLoading } = useCanPublishCustomBlock(workspaceId)
const { data: blocks = [] } = useCustomBlocks(workspaceId)
const { data: workspaces = [] } = useWorkspacesQuery()
// Any org member can view the org's blocks, but publishing requires admin on a
// source workspace — the publish route enforces admin on the picked workspace.
const currentOrgId = useMemo(
() => workspaces.find((w) => w.id === workspaceId)?.organizationId ?? null,
[workspaces, workspaceId]
)
const canCreate = useMemo(
() =>
!!currentOrgId &&
workspaces.some((w) => w.organizationId === currentOrgId && w.permissions === 'admin'),
[workspaces, currentOrgId]
)
const { data: whitelabel } = useWhitelabelSettings(blocks[0]?.organizationId)
const fallbackIconUrl = whitelabel?.logoUrl ?? null
const [searchTerm, setSearchTerm] = useState('')
const [selected, setSelected] = useState<string | 'new' | null>(null)
const filtered = useMemo(() => {
if (!searchTerm.trim()) return blocks
const q = searchTerm.toLowerCase()
return blocks.filter((b) => b.name.toLowerCase().includes(q))
}, [blocks, searchTerm])
if (isLoading) return null
if (!canManage) {
return (
<SettingsEmptyState>
Custom blocks require an Enterprise plan. Contact your admin to enable them.
</SettingsEmptyState>
)
}
if (selected !== null && workspaceId) {
return (
<CustomBlockDetail
key={selected}
blockId={selected === 'new' ? null : selected}
workspaceId={workspaceId}
onBack={() => setSelected(null)}
/>
)
}
return (
<SettingsPanel
search={{
value: searchTerm,
onChange: setSearchTerm,
placeholder: 'Search custom blocks...',
}}
actions={
canCreate
? [
{
text: 'Create block',
icon: Plus,
variant: 'primary',
onSelect: () => setSelected('new'),
},
]
: []
}
>
<SettingsSection label={`Blocks (${blocks.length})`}>
{blocks.length === 0 ? (
<SettingsEmptyState variant='inline'>
{canCreate
? 'No custom blocks yet. Click "Create block" to publish a workflow as a block.'
: 'No custom blocks yet.'}
</SettingsEmptyState>
) : filtered.length === 0 ? (
<SettingsEmptyState variant='inline'>
No blocks found matching "{searchTerm}"
</SettingsEmptyState>
) : (
<div className='-mx-2 flex flex-col gap-y-0.5'>
{filtered.map((cb) => {
const Icon = getCustomBlockIcon(cb.iconUrl, fallbackIconUrl)
return (
<button
key={cb.id}
type='button'
onClick={() => setSelected(cb.id)}
className='w-full rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
>
<SettingsResourceRow
icon={<Icon />}
iconFill
title={cb.name}
description={cb.description || undefined}
trailing={
<div className='flex flex-shrink-0 items-center gap-2'>
{!cb.enabled && <ChipTag variant='gray'>Disabled</ChipTag>}
<ArrowRight className='size-4 text-[var(--text-icon)]' />
</div>
}
/>
</button>
)
})}
</div>
)}
</SettingsSection>
</SettingsPanel>
)
}