diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index d8c905560..79087c7c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -45,7 +45,7 @@ export function CredentialSelector({ previewValue, }: CredentialSelectorProps) { const [showOAuthModal, setShowOAuthModal] = useState(false) - const [inputValue, setInputValue] = useState('') + const [editingValue, setEditingValue] = useState('') const [isEditing, setIsEditing] = useState(false) const { activeWorkflowId } = useWorkflowRegistry() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) @@ -128,11 +128,7 @@ export function CredentialSelector({ return '' }, [selectedCredentialSet, isForeignCredentialSet, selectedCredential, isForeign]) - useEffect(() => { - if (!isEditing) { - setInputValue(resolvedLabel) - } - }, [resolvedLabel, isEditing]) + const displayValue = isEditing ? editingValue : resolvedLabel const invalidSelection = !isPreview && @@ -295,7 +291,7 @@ export function CredentialSelector({ const selectedCredentialProvider = selectedCredential?.provider ?? provider const overlayContent = useMemo(() => { - if (!inputValue) return null + if (!displayValue) return null if (isCredentialSetSelected && selectedCredentialSet) { return ( @@ -303,7 +299,7 @@ export function CredentialSelector({
- {inputValue} + {displayValue} ) } @@ -313,12 +309,12 @@ export function CredentialSelector({
{getProviderIcon(selectedCredentialProvider)}
- {inputValue} + {displayValue} ) }, [ getProviderIcon, - inputValue, + displayValue, selectedCredentialProvider, isCredentialSetSelected, selectedCredentialSet, @@ -335,7 +331,6 @@ export function CredentialSelector({ const credentialSetId = value.slice(CREDENTIAL_SET.PREFIX.length) const matchedSet = credentialSets.find((cs) => cs.id === credentialSetId) if (matchedSet) { - setInputValue(matchedSet.name) handleCredentialSetSelect(credentialSetId) return } @@ -343,13 +338,12 @@ export function CredentialSelector({ const matchedCred = credentials.find((c) => c.id === value) if (matchedCred) { - setInputValue(matchedCred.name) handleSelect(value) return } setIsEditing(true) - setInputValue(value) + setEditingValue(value) }, [credentials, credentialSets, handleAddCredential, handleSelect, handleCredentialSetSelect] ) @@ -359,7 +353,7 @@ export function CredentialSelector({ { setSelectedServerId(serverId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index f0b749f68..b9a3dc5be 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -63,6 +63,7 @@ import { ssoKeys, useSSOProviders } from '@/ee/sso/hooks/sso' import { generalSettingsKeys, useGeneralSettings } from '@/hooks/queries/general-settings' import { organizationKeys, useOrganizations } from '@/hooks/queries/organization' import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription' +import { useSuperUserStatus } from '@/hooks/queries/user-profile' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsModalStore } from '@/stores/modals/settings/store' @@ -204,13 +205,13 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { const [activeSection, setActiveSection] = useState('general') const { initialSection, mcpServerId, clearInitialState } = useSettingsModalStore() const [pendingMcpServerId, setPendingMcpServerId] = useState(null) - const [isSuperUser, setIsSuperUser] = useState(false) const { data: session } = useSession() const queryClient = useQueryClient() const { data: organizationsData } = useOrganizations() const { data: generalSettings } = useGeneralSettings() const { data: subscriptionData } = useSubscriptionData({ enabled: isBillingEnabled }) const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders() + const { data: superUserData } = useSuperUserStatus(session?.user?.id) const activeOrganization = organizationsData?.activeOrganization const { config: permissionConfig } = usePermissionConfig() @@ -229,22 +230,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { const hasEnterprisePlan = subscriptionStatus.isEnterprise const hasOrganization = !!activeOrganization?.id - // Fetch superuser status - useEffect(() => { - const fetchSuperUserStatus = async () => { - if (!userId) return - try { - const response = await fetch('/api/user/super-user') - if (response.ok) { - const data = await response.json() - setIsSuperUser(data.isSuperUser) - } - } catch { - setIsSuperUser(false) - } - } - fetchSuperUserStatus() - }, [userId]) + const isSuperUser = superUserData?.isSuperUser ?? false // Memoize SSO provider ownership check const isSSOProviderOwner = useMemo(() => { @@ -328,7 +314,13 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { generalSettings?.superUserModeEnabled, ]) - // Memoized callbacks to prevent infinite loops in child components + const effectiveActiveSection = useMemo(() => { + if (!isBillingEnabled && (activeSection === 'subscription' || activeSection === 'team')) { + return 'general' + } + return activeSection + }, [activeSection]) + const registerEnvironmentBeforeLeaveHandler = useCallback( (handler: (onProceed: () => void) => void) => { environmentBeforeLeaveHandler.current = handler @@ -342,19 +334,18 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { const handleSectionChange = useCallback( (sectionId: SettingsSection) => { - if (sectionId === activeSection) return + if (sectionId === effectiveActiveSection) return - if (activeSection === 'environment' && environmentBeforeLeaveHandler.current) { + if (effectiveActiveSection === 'environment' && environmentBeforeLeaveHandler.current) { environmentBeforeLeaveHandler.current(() => setActiveSection(sectionId)) return } setActiveSection(sectionId) }, - [activeSection] + [effectiveActiveSection] ) - // Apply initial section from store when modal opens useEffect(() => { if (open && initialSection) { setActiveSection(initialSection) @@ -365,7 +356,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { } }, [open, initialSection, mcpServerId, clearInitialState]) - // Clear pending server ID when section changes away from MCP useEffect(() => { if (activeSection !== 'mcp') { setPendingMcpServerId(null) @@ -391,14 +381,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { } }, [onOpenChange]) - // Redirect away from billing tabs if billing is disabled - useEffect(() => { - if (!isBillingEnabled && (activeSection === 'subscription' || activeSection === 'team')) { - setActiveSection('general') - } - }, [activeSection]) - - // Prefetch functions for React Query const prefetchGeneral = () => { queryClient.prefetchQuery({ queryKey: generalSettingsKeys.settings(), @@ -489,9 +471,17 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { // Handle dialog close - delegate to environment component if it's active const handleDialogOpenChange = (newOpen: boolean) => { - if (!newOpen && activeSection === 'environment' && environmentBeforeLeaveHandler.current) { + if ( + !newOpen && + effectiveActiveSection === 'environment' && + environmentBeforeLeaveHandler.current + ) { environmentBeforeLeaveHandler.current(() => onOpenChange(false)) - } else if (!newOpen && activeSection === 'integrations' && integrationsCloseHandler.current) { + } else if ( + !newOpen && + effectiveActiveSection === 'integrations' && + integrationsCloseHandler.current + ) { integrationsCloseHandler.current(newOpen) } else { onOpenChange(newOpen) @@ -522,7 +512,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { {sectionItems.map((item) => ( } onMouseEnter={() => handlePrefetch(item.id)} onClick={() => handleSectionChange(item.id)} @@ -538,35 +528,36 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { - {navigationItems.find((item) => item.id === activeSection)?.label || activeSection} + {navigationItems.find((item) => item.id === effectiveActiveSection)?.label || + effectiveActiveSection} - {activeSection === 'general' && } - {activeSection === 'environment' && ( + {effectiveActiveSection === 'general' && } + {effectiveActiveSection === 'environment' && ( )} - {activeSection === 'template-profile' && } - {activeSection === 'integrations' && ( + {effectiveActiveSection === 'template-profile' && } + {effectiveActiveSection === 'integrations' && ( )} - {activeSection === 'credential-sets' && } - {activeSection === 'access-control' && } - {activeSection === 'apikeys' && } - {activeSection === 'files' && } - {isBillingEnabled && activeSection === 'subscription' && } - {isBillingEnabled && activeSection === 'team' && } - {activeSection === 'sso' && } - {activeSection === 'byok' && } - {activeSection === 'copilot' && } - {activeSection === 'mcp' && } - {activeSection === 'custom-tools' && } - {activeSection === 'workflow-mcp-servers' && } - {activeSection === 'debug' && } + {effectiveActiveSection === 'credential-sets' && } + {effectiveActiveSection === 'access-control' && } + {effectiveActiveSection === 'apikeys' && } + {effectiveActiveSection === 'files' && } + {isBillingEnabled && effectiveActiveSection === 'subscription' && } + {isBillingEnabled && effectiveActiveSection === 'team' && } + {effectiveActiveSection === 'sso' && } + {effectiveActiveSection === 'byok' && } + {effectiveActiveSection === 'copilot' && } + {effectiveActiveSection === 'mcp' && } + {effectiveActiveSection === 'custom-tools' && } + {effectiveActiveSection === 'workflow-mcp-servers' && } + {effectiveActiveSection === 'debug' && } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx index 7cca37364..989532c28 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx @@ -231,6 +231,8 @@ export function FolderItem({ const isFolderSelected = store.selectedFolders.has(folder.id) if (!isFolderSelected) { + // Replace selection with just this folder (Finder/Explorer pattern) + store.clearAllSelection() store.selectFolder(folder.id) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx index 3c099da60..6963464d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx @@ -189,6 +189,9 @@ export function WorkflowItem({ const isCurrentlySelected = store.selectedWorkflows.has(workflow.id) if (!isCurrentlySelected) { + // Replace selection with just this item (Finder/Explorer pattern) + // This clears both workflow and folder selections + store.clearAllSelection() store.selectWorkflow(workflow.id) } diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index d84200d21..e925b3d19 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -260,6 +260,9 @@ const Popover: React.FC = ({ setIsKeyboardNav(false) setSelectedIndex(-1) registeredItemsRef.current = [] + } else { + // Reset hover state when opening to prevent stale submenu from previous menu + setLastHoveredItem(null) } }, [open]) diff --git a/apps/sim/hooks/queries/user-profile.ts b/apps/sim/hooks/queries/user-profile.ts index f01cbe585..0b3e048b8 100644 --- a/apps/sim/hooks/queries/user-profile.ts +++ b/apps/sim/hooks/queries/user-profile.ts @@ -9,6 +9,7 @@ const logger = createLogger('UserProfileQuery') export const userProfileKeys = { all: ['userProfile'] as const, profile: () => [...userProfileKeys.all, 'profile'] as const, + superUser: (userId?: string) => [...userProfileKeys.all, 'superUser', userId ?? ''] as const, } /** @@ -109,3 +110,37 @@ export function useUpdateUserProfile() { }, }) } + +/** + * Superuser status response type + */ +interface SuperUserStatus { + isSuperUser: boolean +} + +/** + * Fetch superuser status from API + */ +async function fetchSuperUserStatus(): Promise { + const response = await fetch('/api/user/super-user') + + if (!response.ok) { + return { isSuperUser: false } + } + + const data = await response.json() + return { isSuperUser: data.isSuperUser ?? false } +} + +/** + * Hook to fetch superuser status + * @param userId - User ID for cache isolation (required for proper per-user caching) + */ +export function useSuperUserStatus(userId?: string) { + return useQuery({ + queryKey: userProfileKeys.superUser(userId), + queryFn: fetchSuperUserStatus, + enabled: Boolean(userId), + staleTime: 5 * 60 * 1000, // 5 minutes - superuser status rarely changes + }) +}