Skip to content

Commit 91b29b3

Browse files
v0.7.35: sidebar styling, custom blocks reserved names fix, client env var checks
2 parents a403d05 + db2fb3c commit 91b29b3

21 files changed

Lines changed: 365 additions & 71 deletions

File tree

apps/sim/app/account/settings/[section]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
parseSettingsPathSection,
1111
} from '@/components/settings/navigation'
1212
import { getSession } from '@/lib/auth'
13-
import { isBillingEnabled } from '@/lib/core/config/env-flags'
13+
import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags'
1414
import { isPlatformAdmin } from '@/lib/permissions/super-user'
1515

1616
interface AccountSettingsSectionPageProps {
@@ -46,6 +46,7 @@ export default async function AccountSettingsSectionPage({
4646
})
4747
if (!parsed) notFound()
4848
if (parsed === 'billing' && !isBillingEnabled) redirect(getAccountSettingsHref('general'))
49+
if (parsed === 'copilot' && !isHosted) redirect(getAccountSettingsHref('general'))
4950
if (parsed === 'admin' || parsed === 'mothership') {
5051
const isSuperUser = await isPlatformAdmin(session.user.id)
5152
if (!isSuperUser) notFound()

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,13 @@ export function ToolCallItem({
9393
return (
9494
<div className='flex items-center gap-[6px] pl-6'>
9595
{BlockIcon && (
96-
<BlockIcon className='size-[14px] flex-shrink-0' style={getBareIconStyle(BlockIcon)} />
96+
// Size via inline style: a custom block's image icon carries a trailing
97+
// `size-full` that defeats size *classes* (it fills tiled surfaces), so a
98+
// class-only size renders the uploaded icon at natural size here.
99+
<BlockIcon
100+
className='size-[14px] flex-shrink-0'
101+
style={{ width: 14, height: 14, ...getBareIconStyle(BlockIcon) }}
102+
/>
97103
)}
98104
{isExecuting ? (
99105
<ShimmerText className='text-[13px] [--shimmer-rest:var(--text-secondary)]'>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export function SearchModal({
9797
const params = useParams()
9898
const router = useRouter()
9999
const workspaceId = params.workspaceId as string
100+
const currentWorkflowId = params.workflowId as string | undefined
100101
const inputRef = useRef<HTMLInputElement>(null)
101102
const [mounted, setMounted] = useState(false)
102103
const { navigateToSettings } = useSettingsNavigation()
@@ -581,23 +582,25 @@ export function SearchModal({
581582
*/
582583
const filteredBlocks = useMemo(() => {
583584
if (!isOnWorkflowPage) return []
585+
// A custom block is hidden on its own source workflow's canvas — placing it
586+
// there recurses (same exclusion as the toolbar).
584587
return filterAndCap(
585-
blocks,
588+
blocks.filter((b) => !b.sourceWorkflowId || b.sourceWorkflowId !== currentWorkflowId),
586589
(b) => b.name,
587590
deferredSearch,
588591
(b) => b.searchValue
589592
)
590-
}, [isOnWorkflowPage, blocks, deferredSearch])
593+
}, [isOnWorkflowPage, blocks, deferredSearch, currentWorkflowId])
591594

592595
const filteredTools = useMemo(() => {
593596
if (!isOnWorkflowPage) return []
594597
return filterAndCap(
595-
tools,
598+
tools.filter((t) => !t.sourceWorkflowId || t.sourceWorkflowId !== currentWorkflowId),
596599
(t) => t.name,
597600
deferredSearch,
598601
(t) => t.searchValue
599602
)
600-
}, [isOnWorkflowPage, tools, deferredSearch])
603+
}, [isOnWorkflowPage, tools, deferredSearch, currentWorkflowId])
601604

602605
const filteredTriggers = useMemo(() => {
603606
if (!isOnWorkflowPage) return []

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { ChevronDown, ChipConfirmModal, chipVariants, cn } from '@sim/emcn'
55
import { useQueryClient } from '@tanstack/react-query'
66
import { useParams, usePathname, useRouter } from 'next/navigation'
7+
import { ORGANIZATION_PLANE_UNIFIED_SECTIONS } from '@/components/settings/navigation'
78
import { useSession } from '@/lib/auth/auth-client'
89
import { getSubscriptionAccessState } from '@/lib/billing/client'
910
import { canManageWorkspaceBilling } from '@/lib/billing/workspace-permissions'
@@ -120,6 +121,15 @@ export function SettingsSidebar({
120121
}
121122

122123
if (item.selfHostedOverride && !isHosted) {
124+
/**
125+
* Org-plane sections route through the organization gate in
126+
* `settings/[section]/page.tsx` (host organization + org-admin viewer),
127+
* which 404s other viewers — mirror it here so the item never links to
128+
* a dead page.
129+
*/
130+
if (ORGANIZATION_PLANE_UNIFIED_SECTIONS.has(item.id) && !isOrgAdminOrOwner) {
131+
return false
132+
}
123133
if (item.id === 'sso') {
124134
const hasProviders = (ssoProvidersData?.providers?.length ?? 0) > 0
125135
return !hasProviders || isSSOProviderOwner === true

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx

Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { memo, useEffect, useRef, useState } from 'react'
3+
import { memo, type ReactElement, useEffect, useRef, useState } from 'react'
44
import {
55
ChevronDown,
66
Chip,
@@ -14,6 +14,7 @@ import {
1414
Plus,
1515
Send,
1616
Skeleton,
17+
Tooltip,
1718
} from '@sim/emcn'
1819
import { ManageWorkspace, PanelLeft } from '@sim/emcn/icons'
1920
import { createLogger } from '@sim/logger'
@@ -33,6 +34,27 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
3334

3435
const logger = createLogger('WorkspaceHeader')
3536

37+
interface DisabledReasonTooltipProps {
38+
reason: string | null
39+
children: ReactElement
40+
}
41+
42+
/**
43+
* Wraps a menu item in a tooltip explaining why the action is unavailable.
44+
* Renders the child as-is when there is no reason to show.
45+
*/
46+
function DisabledReasonTooltip({ reason, children }: DisabledReasonTooltipProps) {
47+
if (!reason) return children
48+
return (
49+
<Tooltip.Root>
50+
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
51+
<Tooltip.Content>
52+
<p>{reason}</p>
53+
</Tooltip.Content>
54+
</Tooltip.Root>
55+
)
56+
}
57+
3658
interface WorkspaceHeaderProps {
3759
/** The active workspace object */
3860
activeWorkspace?: { name: string } | null
@@ -548,62 +570,67 @@ function WorkspaceHeaderImpl({
548570
<DropdownMenuSeparator className='mx-0' />
549571

550572
<div className='flex flex-col gap-0.5'>
573+
<DisabledReasonTooltip reason={createWorkspaceDisabledReason}>
574+
<Chip
575+
leftIcon={Plus}
576+
onClick={(e) => {
577+
e.stopPropagation()
578+
if (!canCreateWorkspace) return
579+
setIsWorkspaceMenuOpen(false)
580+
setIsCreateModalOpen(true)
581+
}}
582+
disabled={isCreatingWorkspace}
583+
aria-disabled={!canCreateWorkspace || undefined}
584+
fullWidth
585+
flush
586+
className={cn(
587+
'select-none',
588+
!canCreateWorkspace &&
589+
'cursor-not-allowed opacity-60 hover-hover:bg-transparent'
590+
)}
591+
>
592+
New workspace
593+
</Chip>
594+
</DisabledReasonTooltip>
595+
</div>
596+
597+
<DropdownMenuSeparator className='mx-0' />
598+
<DisabledReasonTooltip reason={inviteDisabledReason}>
551599
<Chip
552-
leftIcon={Plus}
553-
onClick={(e) => {
554-
e.stopPropagation()
600+
leftIcon={Send}
601+
onClick={() => {
555602
setIsWorkspaceMenuOpen(false)
556-
if (!canCreateWorkspace) {
603+
if (isInvitationsDisabled) {
557604
if (isBillingEnabled) navigateToSettings({ section: 'billing' })
558605
return
559606
}
560-
setIsCreateModalOpen(true)
607+
setIsInviteModalOpen(true)
561608
}}
562-
disabled={isCreatingWorkspace}
563-
title={createWorkspaceDisabledReason ?? undefined}
564609
fullWidth
565610
flush
566-
className='w-full select-none disabled:pointer-events-none disabled:opacity-50'
611+
className='select-none'
567612
>
568-
New workspace
613+
Invite teammates
569614
</Chip>
570-
</div>
571-
572-
<DropdownMenuSeparator className='mx-0' />
573-
<Chip
574-
leftIcon={Send}
575-
onClick={() => {
576-
setIsWorkspaceMenuOpen(false)
577-
if (isInvitationsDisabled) {
578-
if (isBillingEnabled) navigateToSettings({ section: 'billing' })
579-
return
580-
}
581-
setIsInviteModalOpen(true)
582-
}}
583-
title={inviteDisabledReason ?? undefined}
584-
fullWidth
585-
flush
586-
className='w-full select-none'
587-
>
588-
Invite teammates
589-
</Chip>
590-
<Chip
591-
leftIcon={ManageWorkspace}
592-
onClick={() => {
593-
setIsWorkspaceMenuOpen(false)
594-
if (isInvitationsDisabled) {
595-
if (isBillingEnabled) navigateToSettings({ section: 'billing' })
596-
return
597-
}
598-
navigateToSettings({ section: 'teammates' })
599-
}}
600-
title={inviteDisabledReason ?? undefined}
601-
fullWidth
602-
flush
603-
className='w-full select-none'
604-
>
605-
Manage workspace
606-
</Chip>
615+
</DisabledReasonTooltip>
616+
<DisabledReasonTooltip reason={inviteDisabledReason}>
617+
<Chip
618+
leftIcon={ManageWorkspace}
619+
onClick={() => {
620+
setIsWorkspaceMenuOpen(false)
621+
if (isInvitationsDisabled) {
622+
if (isBillingEnabled) navigateToSettings({ section: 'billing' })
623+
return
624+
}
625+
navigateToSettings({ section: 'teammates' })
626+
}}
627+
fullWidth
628+
flush
629+
className='select-none'
630+
>
631+
Manage workspace
632+
</Chip>
633+
</DisabledReasonTooltip>
607634
</>
608635
)}
609636
</DropdownMenuContent>

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CUSTOM_BLOCK_TILE_COLOR,
99
type CustomBlockRow,
1010
isCustomBlockType,
11+
isReservedOutputName,
1112
} from '@/blocks/custom/build-config'
1213
import type { BlockIcon } from '@/blocks/types'
1314

@@ -33,6 +34,18 @@ describe('isCustomBlockType', () => {
3334
})
3435
})
3536

37+
describe('isReservedOutputName', () => {
38+
it('rejects the system output fields case-insensitively', () => {
39+
expect(isReservedOutputName('cost')).toBe(true)
40+
expect(isReservedOutputName('Cost')).toBe(true)
41+
expect(isReservedOutputName(' success ')).toBe(true)
42+
expect(isReservedOutputName('error')).toBe(true)
43+
expect(isReservedOutputName('result')).toBe(false)
44+
expect(isReservedOutputName('cost_2')).toBe(false)
45+
expect(isReservedOutputName('summary')).toBe(false)
46+
})
47+
})
48+
3649
describe('buildCustomBlockConfig', () => {
3750
const fields: WorkflowInputField[] = [
3851
{ name: 'title', type: 'string' },
@@ -47,6 +60,7 @@ describe('buildCustomBlockConfig', () => {
4760
const config = buildCustomBlockConfig(row, fields, { icon })
4861
expect(config.type).toBe('custom_block_abc123')
4962
expect(config.name).toBe('Invoice Parser')
63+
expect(config.sourceWorkflowId).toBe('wf-1')
5064
expect(config.category).toBe('tools')
5165
expect(config.bgColor).toBe(CUSTOM_BLOCK_TILE_COLOR)
5266
expect(config.hideFromToolbar).toBeUndefined()

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ export const RESERVED_PARAMS = new Set([
6262
'advancedMode',
6363
])
6464

65+
/**
66+
* Output names the block projects itself (`success`/`error` from `buildOutputs`,
67+
* `cost` from the executor's billing aggregation). A user-named exposed output
68+
* must never shadow these — an output literally named `cost` would clobber the
69+
* billed cost. `result` is deliberately NOT reserved: it only exists as a system
70+
* field when no outputs are curated, which cannot co-occur with a named output.
71+
*/
72+
export const RESERVED_OUTPUT_NAMES = new Set(['success', 'error', 'cost'])
73+
74+
/** Whether an exposed-output name collides with a system output field. */
75+
export function isReservedOutputName(name: string): boolean {
76+
return RESERVED_OUTPUT_NAMES.has(name.trim().toLowerCase())
77+
}
78+
6579
/** Map a Start input field type to the editor sub-block type used to collect it. */
6680
function subBlockTypeForField(fieldType: string): SubBlockType {
6781
switch (fieldType) {
@@ -122,6 +136,7 @@ export function buildCustomBlockConfig(
122136
type: row.type,
123137
name: row.name,
124138
description: row.description,
139+
sourceWorkflowId: row.workflowId,
125140
category: 'tools',
126141
longDescription:
127142
'A published workflow packaged as a reusable, self-contained block. Fill its input ' +

apps/sim/blocks/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
468468
}
469469
}
470470
hideFromToolbar?: boolean
471+
/**
472+
* For published custom blocks only: the bound source workflow's id. Discovery
473+
* surfaces use it to hide a workflow's own block on that workflow's canvas
474+
* (placing it would recurse).
475+
*/
476+
sourceWorkflowId?: string
471477
/**
472478
* Marks an unreleased block. Preview blocks are hidden from every discovery
473479
* surface (toolbar, search, mentions, copilot/VFS, docs) in every environment —

apps/sim/blocks/utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ function getProviderFromStore(model: string): string | null {
159159
return null
160160
}
161161

162+
/**
163+
* Whether an Ollama instance is available. `isOllamaConfigured` reads the
164+
* server-only `OLLAMA_URL` env var, which is always undefined in the browser —
165+
* there the providers store (populated from the server's model list, which is
166+
* non-empty only when Ollama is configured) is the signal.
167+
*/
168+
function isOllamaAvailable(): boolean {
169+
if (isOllamaConfigured) return true
170+
return useProvidersStore.getState().providers.ollama.models.length > 0
171+
}
172+
162173
function buildModelVisibilityCondition(model: string, shouldShow: boolean) {
163174
if (!model) {
164175
return { field: 'model', value: '__no_model_selected__' }
@@ -197,7 +208,7 @@ function shouldRequireApiKeyForModel(model: string): boolean {
197208
return false
198209
if (storeProvider) return true
199210

200-
if (isOllamaConfigured) {
211+
if (isOllamaAvailable()) {
201212
if (normalizedModel.includes('/')) return true
202213
if (normalizedModel in getBaseModelProviders()) return true
203214
return false

apps/sim/components/settings/navigation.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getOrganizationSettingsHref,
1212
getWorkspaceSettingsHref,
1313
isOrganizationSettingsSectionAvailable,
14+
ORGANIZATION_PLANE_UNIFIED_SECTIONS,
1415
ORGANIZATION_SETTINGS_ITEMS,
1516
ORGANIZATION_SETTINGS_PATH_ALIASES,
1617
parseSettingsPathSection,
@@ -109,6 +110,19 @@ describe('settings navigation boundaries', () => {
109110
expect([...workspaceIds].sort()).toEqual(WORKSPACE_SETTINGS_ITEMS.map(({ id }) => id).sort())
110111
})
111112

113+
it('derives the organization-plane unified sections from the registry', () => {
114+
expect([...ORGANIZATION_PLANE_UNIFIED_SECTIONS].sort()).toEqual([
115+
'access-control',
116+
'audit-logs',
117+
'billing',
118+
'data-drains',
119+
'data-retention',
120+
'organization',
121+
'sso',
122+
'whitelabeling',
123+
])
124+
})
125+
112126
it('shares labels, icons, and docs links across projections', () => {
113127
const unifiedSso = buildUnifiedSettingsNavigation().find(({ id }) => id === 'sso')
114128
const organizationSso = ORGANIZATION_SETTINGS_ITEMS.find(({ id }) => id === 'sso')

0 commit comments

Comments
 (0)