fix(sidebar): right-click replaces selection, reset popover hover state (#3123)

* fix(sidebar): right-click replaces selection, reset popover hover state

* fix(queries): add userId to superuser query key for cache isolation
This commit is contained in:
Waleed
2026-02-03 10:09:03 -08:00
committed by GitHub
parent f21fe2309c
commit 710bf75bca
7 changed files with 95 additions and 66 deletions

View File

@@ -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<string | null>(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({
<div className='mr-2 flex-shrink-0 opacity-90'>
<Users className='h-3 w-3' />
</div>
<span className='truncate'>{inputValue}</span>
<span className='truncate'>{displayValue}</span>
</div>
)
}
@@ -313,12 +309,12 @@ export function CredentialSelector({
<div className='mr-2 flex-shrink-0 opacity-90'>
{getProviderIcon(selectedCredentialProvider)}
</div>
<span className='truncate'>{inputValue}</span>
<span className='truncate'>{displayValue}</span>
</div>
)
}, [
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({
<Combobox
options={comboboxOptions}
groups={comboboxGroups}
value={inputValue}
value={displayValue}
selectedValue={rawSelectedId}
onChange={handleComboboxChange}
onOpenChange={handleOpenChange}

View File

@@ -673,6 +673,7 @@ export function MCP({ initialServerId }: MCPProps) {
/**
* Opens the detail view for a specific server.
* Note: Tool refresh is handled by the useEffect that watches selectedServerId
*/
const handleViewDetails = useCallback((serverId: string) => {
setSelectedServerId(serverId)

View File

@@ -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<SettingsSection>('general')
const { initialSection, mcpServerId, clearInitialState } = useSettingsModalStore()
const [pendingMcpServerId, setPendingMcpServerId] = useState<string | null>(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) => (
<SModalSidebarItem
key={item.id}
active={activeSection === item.id}
active={effectiveActiveSection === item.id}
icon={<item.icon />}
onMouseEnter={() => handlePrefetch(item.id)}
onClick={() => handleSectionChange(item.id)}
@@ -538,35 +528,36 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
<SModalMain>
<SModalMainHeader>
{navigationItems.find((item) => item.id === activeSection)?.label || activeSection}
{navigationItems.find((item) => item.id === effectiveActiveSection)?.label ||
effectiveActiveSection}
</SModalMainHeader>
<SModalMainBody>
{activeSection === 'general' && <General onOpenChange={onOpenChange} />}
{activeSection === 'environment' && (
{effectiveActiveSection === 'general' && <General onOpenChange={onOpenChange} />}
{effectiveActiveSection === 'environment' && (
<EnvironmentVariables
registerBeforeLeaveHandler={registerEnvironmentBeforeLeaveHandler}
/>
)}
{activeSection === 'template-profile' && <TemplateProfile />}
{activeSection === 'integrations' && (
{effectiveActiveSection === 'template-profile' && <TemplateProfile />}
{effectiveActiveSection === 'integrations' && (
<Integrations
onOpenChange={onOpenChange}
registerCloseHandler={registerIntegrationsCloseHandler}
/>
)}
{activeSection === 'credential-sets' && <CredentialSets />}
{activeSection === 'access-control' && <AccessControl />}
{activeSection === 'apikeys' && <ApiKeys onOpenChange={onOpenChange} />}
{activeSection === 'files' && <FileUploads />}
{isBillingEnabled && activeSection === 'subscription' && <Subscription />}
{isBillingEnabled && activeSection === 'team' && <TeamManagement />}
{activeSection === 'sso' && <SSO />}
{activeSection === 'byok' && <BYOK />}
{activeSection === 'copilot' && <Copilot />}
{activeSection === 'mcp' && <MCP initialServerId={pendingMcpServerId} />}
{activeSection === 'custom-tools' && <CustomTools />}
{activeSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
{activeSection === 'debug' && <Debug />}
{effectiveActiveSection === 'credential-sets' && <CredentialSets />}
{effectiveActiveSection === 'access-control' && <AccessControl />}
{effectiveActiveSection === 'apikeys' && <ApiKeys onOpenChange={onOpenChange} />}
{effectiveActiveSection === 'files' && <FileUploads />}
{isBillingEnabled && effectiveActiveSection === 'subscription' && <Subscription />}
{isBillingEnabled && effectiveActiveSection === 'team' && <TeamManagement />}
{effectiveActiveSection === 'sso' && <SSO />}
{effectiveActiveSection === 'byok' && <BYOK />}
{effectiveActiveSection === 'copilot' && <Copilot />}
{effectiveActiveSection === 'mcp' && <MCP initialServerId={pendingMcpServerId} />}
{effectiveActiveSection === 'custom-tools' && <CustomTools />}
{effectiveActiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
{effectiveActiveSection === 'debug' && <Debug />}
</SModalMainBody>
</SModalMain>
</SModalContent>

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -260,6 +260,9 @@ const Popover: React.FC<PopoverProps> = ({
setIsKeyboardNav(false)
setSelectedIndex(-1)
registeredItemsRef.current = []
} else {
// Reset hover state when opening to prevent stale submenu from previous menu
setLastHoveredItem(null)
}
}, [open])

View File

@@ -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<SuperUserStatus> {
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
})
}