From 9c368346b6a7abae9ea00f3b20c4c01d672cb24e Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Thu, 4 Dec 2025 11:15:45 -0800 Subject: [PATCH] fix: modals, settings, panel --- .../w/[workflowId]/components/chat/chat.tsx | 5 +- .../deploy-modal/components/chat/chat.tsx | 9 +- .../components/template/template.tsx | 15 ++- .../components/oauth-required-modal.tsx | 102 +++++++-------- .../w/[workflowId]/components/panel/panel.tsx | 12 -- .../components/api-keys/api-keys.tsx | 12 +- .../components/environment/environment.tsx | 6 +- .../components/general/general.tsx | 69 +++++------ .../components/integrations/integrations.tsx | 2 +- .../components/mcp/components/index.ts | 2 +- .../mcp-server-skeleton.tsx | 5 +- .../server-list-item/server-list-item.tsx | 29 ++++- .../settings-modal/components/mcp/mcp.tsx | 117 ++++++++++++++++++ .../template-profile/template-profile.tsx | 6 +- .../settings-modal/settings-modal.tsx | 22 ++-- apps/sim/components/emcn/components/index.ts | 1 + .../emcn/components/tooltip/tooltip.tsx | 2 +- apps/sim/components/icons.tsx | 4 +- apps/sim/components/ui/tag-input.tsx | 2 +- apps/sim/stores/panel/editor/store.ts | 4 - 20 files changed, 274 insertions(+), 152 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index c7713fed86..fdc5c2812a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -219,6 +219,7 @@ export function Chat() { const [chatMessage, setChatMessage] = useState('') const [promptHistory, setPromptHistory] = useState([]) const [historyIndex, setHistoryIndex] = useState(-1) + const [moreMenuOpen, setMoreMenuOpen] = useState(false) // Refs const inputRef = useRef(null) @@ -836,7 +837,7 @@ export function Chat() {
{/* More menu with actions */} - + ) : ( !open && onClose()}> - - - Additional Access Required - - The "{toolName}" tool requires access to your {providerName} account to function - properly. - - -
-
-
- -
-
-

Connect {providerName}

-

- You need to connect your {providerName} account to continue -

+ + Connect {providerName} + +
+
+
+ +
+
+

+ Connect your {providerName} account +

+

+ The "{toolName}" tool requires access to your account +

+
-
- {displayScopes.length > 0 && ( -
-
-

Permissions requested

+ {displayScopes.length > 0 && ( +
+
+

+ Permissions requested +

+
+
    + {displayScopes.map((scope) => ( +
  • +
    + +
    +
    + {getScopeDescription(scope)} + {newScopesSet.has(scope) && ( + + New + + )} +
    +
  • + ))} +
-
    - {displayScopes.map((scope) => ( -
  • -
    - -
    -
    - {getScopeDescription(scope)} - {newScopesSet.has(scope) && ( - - New - - )} -
    -
  • - ))} -
-
- )} -
+ )} +
+ - diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index bbda443f44..4215af5c2d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -166,18 +166,6 @@ export function Panel() { setHasHydrated(true) }, [setHasHydrated]) - /** - * Focus Copilot user input when the Copilot tab becomes active or when - * the panel loads with Copilot already selected, after hydration. - */ - useEffect(() => { - if (!_hasHydrated || activeTab !== 'copilot') { - return - } - - copilotRef.current?.focusInput() - }, [_hasHydrated, activeTab]) - /** * Handles tab click events */ diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx index cb0d0de5bc..1e2cb3ed0a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx @@ -241,13 +241,13 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) { {isLoading ? (
- +
- +
- +
@@ -624,10 +624,10 @@ function ApiKeySkeleton() {
- - + +
- +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/environment/environment.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/environment/environment.tsx index f1229c7395..c550419781 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/environment/environment.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/environment/environment.tsx @@ -752,13 +752,13 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment {isLoading ? ( <>
- +
- +
- + {Array.from({ length: 2 }, (_, i) => (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/general/general.tsx index 533dbcab1f..bf7f7fdf68 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/general/general.tsx @@ -328,21 +328,28 @@ export function General({ onOpenChange }: GeneralProps) {
{isEditingName ? ( <> - setName(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleInputBlur} - className='w-auto border-0 bg-transparent p-0 font-medium text-[14px] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0' - size={Math.max(name.length, 1)} - maxLength={100} - disabled={updateProfile.isPending} - autoComplete='off' - autoCorrect='off' - autoCapitalize='off' - spellCheck='false' - /> +
+ + setName(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleInputBlur} + className='absolute top-0 left-0 h-full w-full border-0 bg-transparent p-0 font-medium text-[14px] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0' + maxLength={100} + disabled={updateProfile.isPending} + autoComplete='off' + autoCorrect='off' + autoCapitalize='off' + spellCheck='false' + /> +
-

{profile?.email || ''}

+

{profile?.email || ''}

{uploadError &&

{uploadError}

} @@ -507,42 +514,34 @@ function GeneralSkeleton() {
- - + +
- +
- {/* Theme row - temporarily hidden while light mode is disabled */} - {/*
- - -
*/} - {/* Auto-connect row */} -
+
- +
{/* Error notifications row */}
- - + +
{/* Telemetry row */}
- - + +
{/* Telemetry description */} -
- - -
+ + {/* Action buttons */}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/integrations/integrations.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/integrations/integrations.tsx index c6b5e8e9b5..3cb50cd30d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/integrations/integrations.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/integrations/integrations.tsx @@ -313,7 +313,7 @@ export function Integrations({ onOpenChange, registerCloseHandler }: Integration
{Object.entries(filteredGroupedServices).map(([providerKey, providerServices]) => (
-
- +
+ + +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx index 9993b905d6..a4de645a8f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/emcn' /** * Formats transport type for display (e.g., "streamable-http" -> "Streamable-HTTP"). */ -function formatTransportLabel(transport: string): string { +export function formatTransportLabel(transport: string): string { return transport .split('-') .map((word) => @@ -29,9 +29,19 @@ interface ServerListItemProps { tools: any[] isDeleting: boolean onRemove: () => void + onViewDetails: () => void } -export function ServerListItem({ server, tools, isDeleting, onRemove }: ServerListItemProps) { +/** + * Renders a single MCP server list item with details and delete actions. + */ +export function ServerListItem({ + server, + tools, + isDeleting, + onRemove, + onViewDetails, +}: ServerListItemProps) { const transportLabel = formatTransportLabel(server.transport || 'http') const toolsLabel = formatToolsLabel(tools) @@ -46,9 +56,18 @@ export function ServerListItem({ server, tools, isDeleting, onRemove }: ServerLi

{toolsLabel}

- +
+ + +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx index 455df8078f..b393856c91 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx @@ -25,11 +25,25 @@ import type { InputFieldType, McpServerFormData, McpServerTestResult } from './c import { FormattedInput, FormField, + formatTransportLabel, HeaderRow, McpServerSkeleton, ServerListItem, } from './components' +interface McpTool { + name: string + description?: string + serverId: string +} + +interface McpServer { + id: string + name?: string + transport?: string + url?: string +} + const logger = createLogger('McpSettings') const DEFAULT_FORM_DATA: McpServerFormData = { @@ -86,6 +100,9 @@ export function MCP() { const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [serverToDelete, setServerToDelete] = useState<{ id: string; name: string } | null>(null) + // Server details view state + const [selectedServerId, setSelectedServerId] = useState(null) + // Environment variable dropdown state const [showEnvVars, setShowEnvVars] = useState(false) const [envSearchTerm, setEnvSearchTerm] = useState('') @@ -359,6 +376,31 @@ export function MCP() { setShowAddForm(false) }, []) + /** + * Opens the detail view for a specific server. + */ + const handleViewDetails = useCallback((serverId: string) => { + setSelectedServerId(serverId) + }, []) + + /** + * Closes the detail view and returns to the server list. + */ + const handleBackToList = useCallback(() => { + setSelectedServerId(null) + }, []) + + /** + * Gets the selected server and its tools for the detail view. + */ + const selectedServer = useMemo(() => { + if (!selectedServerId) return null + const server = servers.find((s) => s.id === selectedServerId) as McpServer | undefined + if (!server) return null + const serverTools = (toolsByServer[selectedServerId] || []) as McpTool[] + return { server, tools: serverTools } + }, [selectedServerId, servers, toolsByServer]) + const error = toolsError || serversError const hasServers = servers && servers.length > 0 const showEmptyState = !hasServers && !showAddForm @@ -369,6 +411,80 @@ export function MCP() { const isSubmitDisabled = serversLoading || isAddingServer || !isFormValid const testButtonLabel = getTestButtonLabel(testResult, isTestingConnection) + // Show detail view if a server is selected + if (selectedServer) { + const { server, tools } = selectedServer + const transportLabel = formatTransportLabel(server.transport || 'http') + + return ( +
+
+
+
+ + Server Name + +

+ {server.name || 'Unnamed Server'} +

+
+ +
+ Transport +

{transportLabel}

+
+ + {server.url && ( +
+ URL +

+ {server.url} +

+
+ )} + +
+ + Tools ({tools.length}) + + {tools.length === 0 ? ( +

No tools available

+ ) : ( +
+ {tools.map((tool) => ( +
+

+ {tool.name} +

+ {tool.description && ( +

+ {tool.description} +

+ )} +
+ ))} +
+ )} +
+
+
+ +
+ +
+
+ ) + } + return ( <>
@@ -524,6 +640,7 @@ export function MCP() { tools={tools} isDeleting={deletingServers.has(server.id)} onRemove={() => handleRemoveServer(server.id, server.name || 'this server')} + onViewDetails={() => handleViewDetails(server.id)} /> ) })} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/template-profile/template-profile.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/template-profile/template-profile.tsx index f7aa000923..c93feea8e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/template-profile/template-profile.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/template-profile/template-profile.tsx @@ -261,7 +261,7 @@ export function TemplateProfile() {
{/* Display Skeleton */}
- +
@@ -270,13 +270,13 @@ export function TemplateProfile() { {/* About Skeleton */}
- +
{/* Socials Skeleton */}
- + diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/settings-modal.tsx index 51bcd9b228..05456870dd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/settings-modal.tsx @@ -48,6 +48,7 @@ import { ssoKeys, useSSOProviders } from '@/hooks/queries/sso' import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription' const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) +const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) interface SettingsModalProps { open: boolean @@ -167,6 +168,18 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { return false } + // SSO has special logic that must be checked before requiresTeam + if (item.id === 'sso') { + if (isHosted) { + return hasOrganization && hasEnterprisePlan && canManageSSO + } + // For self-hosted, only show SSO tab if explicitly enabled via environment variable + if (!isSSOEnabled) return false + // Show tab if user is the SSO provider owner, or if no providers exist yet (to allow initial setup) + const hasProviders = (ssoProvidersData?.providers?.length ?? 0) > 0 + return !hasProviders || isSSOProviderOwner === true + } + if (item.requiresTeam) { const isMember = userRole === 'member' || isAdmin const hasTeamPlan = subscriptionStatus.isTeam || subscriptionStatus.isEnterprise @@ -185,13 +198,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { return false } - if (item.id === 'sso') { - if (isHosted) { - return hasOrganization && hasEnterprisePlan && canManageSSO - } - return isSSOProviderOwner === true - } - if (item.requiresOwner && !isOwner) { return false } @@ -203,6 +209,8 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { hasEnterprisePlan, canManageSSO, isSSOProviderOwner, + isSSOEnabled, + ssoProvidersData?.providers?.length, isOwner, isAdmin, userRole, diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts index 360217a8e8..792b6a2d86 100644 --- a/apps/sim/components/emcn/components/index.ts +++ b/apps/sim/components/emcn/components/index.ts @@ -13,6 +13,7 @@ export { Input } from './input/input' export { Label } from './label/label' export { Modal, + ModalBody, ModalClose, ModalContent, type ModalContentProps, diff --git a/apps/sim/components/emcn/components/tooltip/tooltip.tsx b/apps/sim/components/emcn/components/tooltip/tooltip.tsx index cea7f90572..705d7a3769 100644 --- a/apps/sim/components/emcn/components/tooltip/tooltip.tsx +++ b/apps/sim/components/emcn/components/tooltip/tooltip.tsx @@ -45,7 +45,7 @@ const Content = React.forwardRef< collisionPadding={8} avoidCollisions={true} className={cn( - 'z-50 rounded-[3px] bg-black px-[7.5px] py-[6px] font-base text-white text-xs shadow-md dark:bg-white dark:text-black', + 'z-[10000300] rounded-[3px] bg-black px-[7.5px] py-[6px] font-base text-white text-xs shadow-md dark:bg-white dark:text-black', className )} {...props} diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 573d1b4490..062d7f479f 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -696,8 +696,8 @@ export function GrafanaIcon(props: SVGProps) { y2='5.356' gradientUnits='userSpaceOnUse' > - - + + diff --git a/apps/sim/components/ui/tag-input.tsx b/apps/sim/components/ui/tag-input.tsx index 4b7cd83cd3..2878230490 100644 --- a/apps/sim/components/ui/tag-input.tsx +++ b/apps/sim/components/ui/tag-input.tsx @@ -75,7 +75,7 @@ export function TagInput({ placeholder={value.length === 0 ? placeholder : ''} disabled={disabled} className={cn( - 'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 text-[13px] focus-visible:ring-0 focus-visible:ring-offset-0', + 'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-sm placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0', value.length > 0 ? 'pl-[4px]' : 'pl-[4px]' )} /> diff --git a/apps/sim/stores/panel/editor/store.ts b/apps/sim/stores/panel/editor/store.ts index 651951d076..75d6098c21 100644 --- a/apps/sim/stores/panel/editor/store.ts +++ b/apps/sim/stores/panel/editor/store.ts @@ -50,10 +50,6 @@ export const usePanelEditorStore = create()( }, clearCurrentBlock: () => { set({ currentBlockId: null }) - - // When selection is cleared (e.g. clicking on the canvas), switch to the toolbar tab - const panelState = usePanelStore.getState() - panelState.setActiveTab('toolbar') }, setConnectionsHeight: (height) => { const clampedHeight = Math.max(