Skip to content

Commit ab9ddb2

Browse files
committed
fix(sidebar): right-click replaces selection, reset popover hover state
1 parent a9b7d75 commit ab9ddb2

File tree

7 files changed

+95
-66
lines changed

7 files changed

+95
-66
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function CredentialSelector({
4545
previewValue,
4646
}: CredentialSelectorProps) {
4747
const [showOAuthModal, setShowOAuthModal] = useState(false)
48-
const [inputValue, setInputValue] = useState('')
48+
const [editingValue, setEditingValue] = useState('')
4949
const [isEditing, setIsEditing] = useState(false)
5050
const { activeWorkflowId } = useWorkflowRegistry()
5151
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
@@ -128,11 +128,7 @@ export function CredentialSelector({
128128
return ''
129129
}, [selectedCredentialSet, isForeignCredentialSet, selectedCredential, isForeign])
130130

131-
useEffect(() => {
132-
if (!isEditing) {
133-
setInputValue(resolvedLabel)
134-
}
135-
}, [resolvedLabel, isEditing])
131+
const displayValue = isEditing ? editingValue : resolvedLabel
136132

137133
const invalidSelection =
138134
!isPreview &&
@@ -295,15 +291,15 @@ export function CredentialSelector({
295291
const selectedCredentialProvider = selectedCredential?.provider ?? provider
296292

297293
const overlayContent = useMemo(() => {
298-
if (!inputValue) return null
294+
if (!displayValue) return null
299295

300296
if (isCredentialSetSelected && selectedCredentialSet) {
301297
return (
302298
<div className='flex w-full items-center truncate'>
303299
<div className='mr-2 flex-shrink-0 opacity-90'>
304300
<Users className='h-3 w-3' />
305301
</div>
306-
<span className='truncate'>{inputValue}</span>
302+
<span className='truncate'>{displayValue}</span>
307303
</div>
308304
)
309305
}
@@ -313,12 +309,12 @@ export function CredentialSelector({
313309
<div className='mr-2 flex-shrink-0 opacity-90'>
314310
{getProviderIcon(selectedCredentialProvider)}
315311
</div>
316-
<span className='truncate'>{inputValue}</span>
312+
<span className='truncate'>{displayValue}</span>
317313
</div>
318314
)
319315
}, [
320316
getProviderIcon,
321-
inputValue,
317+
displayValue,
322318
selectedCredentialProvider,
323319
isCredentialSetSelected,
324320
selectedCredentialSet,
@@ -335,21 +331,19 @@ export function CredentialSelector({
335331
const credentialSetId = value.slice(CREDENTIAL_SET.PREFIX.length)
336332
const matchedSet = credentialSets.find((cs) => cs.id === credentialSetId)
337333
if (matchedSet) {
338-
setInputValue(matchedSet.name)
339334
handleCredentialSetSelect(credentialSetId)
340335
return
341336
}
342337
}
343338

344339
const matchedCred = credentials.find((c) => c.id === value)
345340
if (matchedCred) {
346-
setInputValue(matchedCred.name)
347341
handleSelect(value)
348342
return
349343
}
350344

351345
setIsEditing(true)
352-
setInputValue(value)
346+
setEditingValue(value)
353347
},
354348
[credentials, credentialSets, handleAddCredential, handleSelect, handleCredentialSetSelect]
355349
)
@@ -359,7 +353,7 @@ export function CredentialSelector({
359353
<Combobox
360354
options={comboboxOptions}
361355
groups={comboboxGroups}
362-
value={inputValue}
356+
value={displayValue}
363357
selectedValue={rawSelectedId}
364358
onChange={handleComboboxChange}
365359
onOpenChange={handleOpenChange}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,7 @@ export function MCP({ initialServerId }: MCPProps) {
675675

676676
/**
677677
* Opens the detail view for a specific server.
678+
* Note: Tool refresh is handled by the useEffect that watches selectedServerId
678679
*/
679680
const handleViewDetails = useCallback((serverId: string) => {
680681
setSelectedServerId(serverId)

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

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { generalSettingsKeys, useGeneralSettings } from '@/hooks/queries/general
6363
import { organizationKeys, useOrganizations } from '@/hooks/queries/organization'
6464
import { ssoKeys, useSSOProviders } from '@/hooks/queries/sso'
6565
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
66+
import { useSuperUserStatus } from '@/hooks/queries/user-profile'
6667
import { usePermissionConfig } from '@/hooks/use-permission-config'
6768
import { useSettingsModalStore } from '@/stores/modals/settings/store'
6869

@@ -204,13 +205,13 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
204205
const [activeSection, setActiveSection] = useState<SettingsSection>('general')
205206
const { initialSection, mcpServerId, clearInitialState } = useSettingsModalStore()
206207
const [pendingMcpServerId, setPendingMcpServerId] = useState<string | null>(null)
207-
const [isSuperUser, setIsSuperUser] = useState(false)
208208
const { data: session } = useSession()
209209
const queryClient = useQueryClient()
210210
const { data: organizationsData } = useOrganizations()
211211
const { data: generalSettings } = useGeneralSettings()
212212
const { data: subscriptionData } = useSubscriptionData({ enabled: isBillingEnabled })
213213
const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders()
214+
const { data: superUserData } = useSuperUserStatus(Boolean(session?.user?.id))
214215

215216
const activeOrganization = organizationsData?.activeOrganization
216217
const { config: permissionConfig } = usePermissionConfig()
@@ -229,22 +230,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
229230
const hasEnterprisePlan = subscriptionStatus.isEnterprise
230231
const hasOrganization = !!activeOrganization?.id
231232

232-
// Fetch superuser status
233-
useEffect(() => {
234-
const fetchSuperUserStatus = async () => {
235-
if (!userId) return
236-
try {
237-
const response = await fetch('/api/user/super-user')
238-
if (response.ok) {
239-
const data = await response.json()
240-
setIsSuperUser(data.isSuperUser)
241-
}
242-
} catch {
243-
setIsSuperUser(false)
244-
}
245-
}
246-
fetchSuperUserStatus()
247-
}, [userId])
233+
const isSuperUser = superUserData?.isSuperUser ?? false
248234

249235
// Memoize SSO provider ownership check
250236
const isSSOProviderOwner = useMemo(() => {
@@ -328,7 +314,13 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
328314
generalSettings?.superUserModeEnabled,
329315
])
330316

331-
// Memoized callbacks to prevent infinite loops in child components
317+
const effectiveActiveSection = useMemo(() => {
318+
if (!isBillingEnabled && (activeSection === 'subscription' || activeSection === 'team')) {
319+
return 'general'
320+
}
321+
return activeSection
322+
}, [activeSection])
323+
332324
const registerEnvironmentBeforeLeaveHandler = useCallback(
333325
(handler: (onProceed: () => void) => void) => {
334326
environmentBeforeLeaveHandler.current = handler
@@ -342,19 +334,18 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
342334

343335
const handleSectionChange = useCallback(
344336
(sectionId: SettingsSection) => {
345-
if (sectionId === activeSection) return
337+
if (sectionId === effectiveActiveSection) return
346338

347-
if (activeSection === 'environment' && environmentBeforeLeaveHandler.current) {
339+
if (effectiveActiveSection === 'environment' && environmentBeforeLeaveHandler.current) {
348340
environmentBeforeLeaveHandler.current(() => setActiveSection(sectionId))
349341
return
350342
}
351343

352344
setActiveSection(sectionId)
353345
},
354-
[activeSection]
346+
[effectiveActiveSection]
355347
)
356348

357-
// Apply initial section from store when modal opens
358349
useEffect(() => {
359350
if (open && initialSection) {
360351
setActiveSection(initialSection)
@@ -365,7 +356,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
365356
}
366357
}, [open, initialSection, mcpServerId, clearInitialState])
367358

368-
// Clear pending server ID when section changes away from MCP
369359
useEffect(() => {
370360
if (activeSection !== 'mcp') {
371361
setPendingMcpServerId(null)
@@ -391,14 +381,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
391381
}
392382
}, [onOpenChange])
393383

394-
// Redirect away from billing tabs if billing is disabled
395-
useEffect(() => {
396-
if (!isBillingEnabled && (activeSection === 'subscription' || activeSection === 'team')) {
397-
setActiveSection('general')
398-
}
399-
}, [activeSection])
400-
401-
// Prefetch functions for React Query
402384
const prefetchGeneral = () => {
403385
queryClient.prefetchQuery({
404386
queryKey: generalSettingsKeys.settings(),
@@ -489,9 +471,17 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
489471

490472
// Handle dialog close - delegate to environment component if it's active
491473
const handleDialogOpenChange = (newOpen: boolean) => {
492-
if (!newOpen && activeSection === 'environment' && environmentBeforeLeaveHandler.current) {
474+
if (
475+
!newOpen &&
476+
effectiveActiveSection === 'environment' &&
477+
environmentBeforeLeaveHandler.current
478+
) {
493479
environmentBeforeLeaveHandler.current(() => onOpenChange(false))
494-
} else if (!newOpen && activeSection === 'integrations' && integrationsCloseHandler.current) {
480+
} else if (
481+
!newOpen &&
482+
effectiveActiveSection === 'integrations' &&
483+
integrationsCloseHandler.current
484+
) {
495485
integrationsCloseHandler.current(newOpen)
496486
} else {
497487
onOpenChange(newOpen)
@@ -522,7 +512,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
522512
{sectionItems.map((item) => (
523513
<SModalSidebarItem
524514
key={item.id}
525-
active={activeSection === item.id}
515+
active={effectiveActiveSection === item.id}
526516
icon={<item.icon />}
527517
onMouseEnter={() => handlePrefetch(item.id)}
528518
onClick={() => handleSectionChange(item.id)}
@@ -538,35 +528,36 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
538528

539529
<SModalMain>
540530
<SModalMainHeader>
541-
{navigationItems.find((item) => item.id === activeSection)?.label || activeSection}
531+
{navigationItems.find((item) => item.id === effectiveActiveSection)?.label ||
532+
effectiveActiveSection}
542533
</SModalMainHeader>
543534
<SModalMainBody>
544-
{activeSection === 'general' && <General onOpenChange={onOpenChange} />}
545-
{activeSection === 'environment' && (
535+
{effectiveActiveSection === 'general' && <General onOpenChange={onOpenChange} />}
536+
{effectiveActiveSection === 'environment' && (
546537
<EnvironmentVariables
547538
registerBeforeLeaveHandler={registerEnvironmentBeforeLeaveHandler}
548539
/>
549540
)}
550-
{activeSection === 'template-profile' && <TemplateProfile />}
551-
{activeSection === 'integrations' && (
541+
{effectiveActiveSection === 'template-profile' && <TemplateProfile />}
542+
{effectiveActiveSection === 'integrations' && (
552543
<Integrations
553544
onOpenChange={onOpenChange}
554545
registerCloseHandler={registerIntegrationsCloseHandler}
555546
/>
556547
)}
557-
{activeSection === 'credential-sets' && <CredentialSets />}
558-
{activeSection === 'access-control' && <AccessControl />}
559-
{activeSection === 'apikeys' && <ApiKeys onOpenChange={onOpenChange} />}
560-
{activeSection === 'files' && <FileUploads />}
561-
{isBillingEnabled && activeSection === 'subscription' && <Subscription />}
562-
{isBillingEnabled && activeSection === 'team' && <TeamManagement />}
563-
{activeSection === 'sso' && <SSO />}
564-
{activeSection === 'byok' && <BYOK />}
565-
{activeSection === 'copilot' && <Copilot />}
566-
{activeSection === 'mcp' && <MCP initialServerId={pendingMcpServerId} />}
567-
{activeSection === 'custom-tools' && <CustomTools />}
568-
{activeSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
569-
{activeSection === 'debug' && <Debug />}
548+
{effectiveActiveSection === 'credential-sets' && <CredentialSets />}
549+
{effectiveActiveSection === 'access-control' && <AccessControl />}
550+
{effectiveActiveSection === 'apikeys' && <ApiKeys onOpenChange={onOpenChange} />}
551+
{effectiveActiveSection === 'files' && <FileUploads />}
552+
{isBillingEnabled && effectiveActiveSection === 'subscription' && <Subscription />}
553+
{isBillingEnabled && effectiveActiveSection === 'team' && <TeamManagement />}
554+
{effectiveActiveSection === 'sso' && <SSO />}
555+
{effectiveActiveSection === 'byok' && <BYOK />}
556+
{effectiveActiveSection === 'copilot' && <Copilot />}
557+
{effectiveActiveSection === 'mcp' && <MCP initialServerId={pendingMcpServerId} />}
558+
{effectiveActiveSection === 'custom-tools' && <CustomTools />}
559+
{effectiveActiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
560+
{effectiveActiveSection === 'debug' && <Debug />}
570561
</SModalMainBody>
571562
</SModalMain>
572563
</SModalContent>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ export function FolderItem({
231231
const isFolderSelected = store.selectedFolders.has(folder.id)
232232

233233
if (!isFolderSelected) {
234+
// Replace selection with just this folder (Finder/Explorer pattern)
235+
store.clearAllSelection()
234236
store.selectFolder(folder.id)
235237
}
236238

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ export function WorkflowItem({
189189
const isCurrentlySelected = store.selectedWorkflows.has(workflow.id)
190190

191191
if (!isCurrentlySelected) {
192+
// Replace selection with just this item (Finder/Explorer pattern)
193+
// This clears both workflow and folder selections
194+
store.clearAllSelection()
192195
store.selectWorkflow(workflow.id)
193196
}
194197

apps/sim/components/emcn/components/popover/popover.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ const Popover: React.FC<PopoverProps> = ({
260260
setIsKeyboardNav(false)
261261
setSelectedIndex(-1)
262262
registeredItemsRef.current = []
263+
} else {
264+
// Reset hover state when opening to prevent stale submenu from previous menu
265+
setLastHoveredItem(null)
263266
}
264267
}, [open])
265268

apps/sim/hooks/queries/user-profile.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const logger = createLogger('UserProfileQuery')
99
export const userProfileKeys = {
1010
all: ['userProfile'] as const,
1111
profile: () => [...userProfileKeys.all, 'profile'] as const,
12+
superUser: () => [...userProfileKeys.all, 'superUser'] as const,
1213
}
1314

1415
/**
@@ -109,3 +110,37 @@ export function useUpdateUserProfile() {
109110
},
110111
})
111112
}
113+
114+
/**
115+
* Superuser status response type
116+
*/
117+
interface SuperUserStatus {
118+
isSuperUser: boolean
119+
}
120+
121+
/**
122+
* Fetch superuser status from API
123+
*/
124+
async function fetchSuperUserStatus(): Promise<SuperUserStatus> {
125+
const response = await fetch('/api/user/super-user')
126+
127+
if (!response.ok) {
128+
return { isSuperUser: false }
129+
}
130+
131+
const data = await response.json()
132+
return { isSuperUser: data.isSuperUser ?? false }
133+
}
134+
135+
/**
136+
* Hook to fetch superuser status
137+
*/
138+
export function useSuperUserStatus(enabled = true) {
139+
return useQuery({
140+
queryKey: userProfileKeys.superUser(),
141+
queryFn: fetchSuperUserStatus,
142+
enabled,
143+
staleTime: 5 * 60 * 1000, // 5 minutes - superuser status rarely changes
144+
placeholderData: keepPreviousData,
145+
})
146+
}

0 commit comments

Comments
 (0)