Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,32 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti

return (
<Dialog open={open} onOpenChange={handleCloseModal}>
<DialogContent className='flex h-[70vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[800px]'>
<DialogContent className='relative flex h-[70vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[800px]'>
{/* Hidden dummy inputs to prevent browser password manager autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='password'
name='fakepasswordremembered'
autoComplete='current-password'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='email'
name='fakeemailremembered'
autoComplete='email'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
<DialogTitle className='font-medium text-lg'>Webhook Notifications</DialogTitle>
</DialogHeader>
Expand Down Expand Up @@ -817,19 +842,24 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti
<div className='relative'>
<Input
id='secret'
type={showSecret ? 'text' : 'password'}
type='text'
placeholder='Webhook secret for signature verification'
value={newWebhook.secret}
onChange={(e) => {
setNewWebhook({ ...newWebhook, secret: e.target.value })
setFieldErrors({ ...fieldErrors, general: undefined })
}}
className='h-9 rounded-[8px] pr-32'
autoComplete='new-password'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
data-form-type='other'
style={
!showSecret
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
: undefined
}
/>
<div className='absolute top-0.5 right-0.5 flex h-8 items-center gap-1 pr-1'>
<Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ export function ShortInput({
setShowEnvVars(false)
setSearchTerm('')
}}
maxHeight='192px'
/>
<TagDropdown
visible={showTags}
Expand Down
21 changes: 21 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
import { useCopilotStore } from '@/stores/copilot/store'
import { useExecutionStore } from '@/stores/execution/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { hasWorkflowsInitiallyLoaded, useWorkflowRegistry } from '@/stores/workflows/registry/store'
Expand Down Expand Up @@ -1145,6 +1146,26 @@ const WorkflowContent = React.memo(() => {
setIsWorkflowReady(shouldBeReady)
}, [activeWorkflowId, params.workflowId, workflows, isLoading])

// Preload workspace environment variables when workflow is ready
const loadWorkspaceEnvironment = useEnvironmentStore((state) => state.loadWorkspaceEnvironment)
const clearWorkspaceEnvCache = useEnvironmentStore((state) => state.clearWorkspaceEnvCache)
const prevWorkspaceIdRef = useRef<string | null>(null)

useEffect(() => {
// Only preload if workflow is ready and workspaceId is available
if (!isWorkflowReady || !workspaceId) return

// Clear cache if workspace changed
if (prevWorkspaceIdRef.current && prevWorkspaceIdRef.current !== workspaceId) {
clearWorkspaceEnvCache(prevWorkspaceIdRef.current)
}

// Preload workspace environment (will use cache if available)
void loadWorkspaceEnvironment(workspaceId)

prevWorkspaceIdRef.current = workspaceId
}, [isWorkflowReady, workspaceId, loadWorkspaceEnvironment, clearWorkspaceEnvCache])

// Handle navigation and validation
useEffect(() => {
const validateAndNavigate = async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ export function EnvironmentVariables({
data-input-type='value'
value={envVar.value}
onChange={(e) => updateEnvVar(originalIndex, 'value', e.target.value)}
type={focusedValueIndex === originalIndex ? 'text' : 'password'}
type='text'
onFocus={(e) => {
if (!isConflict) {
e.target.removeAttribute('readOnly')
Expand All @@ -421,10 +421,15 @@ export function EnvironmentVariables({
disabled={isConflict}
aria-disabled={isConflict}
name={`env_variable_value_${envVar.id || originalIndex}_${Math.random()}`}
autoComplete='new-password'
autoComplete='off'
autoCapitalize='off'
spellCheck='false'
readOnly={isConflict}
style={
focusedValueIndex !== originalIndex && !isConflict
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
: undefined
}
className={`allow-scroll h-9 rounded-[8px] border-none px-3 font-normal text-sm ring-0 ring-offset-0 placeholder:text-muted-foreground focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 ${isConflict ? 'cursor-not-allowed border border-red-500 bg-[#F6D2D2] outline-none ring-0 disabled:bg-[#F6D2D2] disabled:opacity-100 dark:bg-[#442929] disabled:dark:bg-[#442929]' : 'bg-muted'}`}
/>
<div className='flex items-center justify-end gap-2'>
Expand Down Expand Up @@ -476,8 +481,31 @@ export function EnvironmentVariables({

return (
<div className='relative flex h-full flex-col'>
{/* Hidden dummy input to prevent autofill */}
<input type='text' name='hidden' style={{ display: 'none' }} autoComplete='false' />
{/* Hidden dummy inputs to prevent browser password manager autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='password'
name='fakepasswordremembered'
autoComplete='current-password'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='email'
name='fakeemailremembered'
autoComplete='email'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
{/* Fixed Header */}
<div className='px-6 pt-4 pb-2'>
{/* Search Input */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,32 @@ export function SSO() {
const showStatus = hasProviders && !showConfigForm

return (
<div className='flex h-full flex-col'>
<div className='relative flex h-full flex-col'>
{/* Hidden dummy inputs to prevent browser password manager autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='password'
name='fakepasswordremembered'
autoComplete='current-password'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='email'
name='fakeemailremembered'
autoComplete='email'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<div className='flex-1 overflow-y-auto px-6 pt-4 pb-4'>
<div className='space-y-6'>
{error && (
Expand Down Expand Up @@ -757,11 +782,11 @@ export function SSO() {
<div className='relative'>
<Input
id='client-secret'
type={showClientSecret ? 'text' : 'password'}
type='text'
placeholder='Enter Client Secret'
value={formData.clientSecret}
name='sso_client_key'
autoComplete='new-password'
autoComplete='off'
autoCapitalize='none'
spellCheck={false}
readOnly
Expand All @@ -771,6 +796,11 @@ export function SSO() {
}}
onBlurCapture={() => setShowClientSecret(false)}
onChange={(e) => handleInputChange('clientSecret', e.target.value)}
style={
!showClientSecret
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
: undefined
}
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showErrors &&
Expand Down
39 changes: 6 additions & 33 deletions apps/sim/components/ui/env-var-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
}>({ workspace: {}, personal: {}, conflicts: [] })
const [selectedIndex, setSelectedIndex] = useState(0)

// Load workspace environment variables when workspaceId changes
useEffect(() => {
if (workspaceId && visible) {
loadWorkspaceEnvironment(workspaceId).then((data) => {
Expand All @@ -51,36 +50,26 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
}
}, [workspaceId, visible, loadWorkspaceEnvironment])

// Combine and organize environment variables
const envVarGroups: EnvVarGroup[] = []

if (workspaceId) {
// When workspaceId is provided, show both workspace and user env vars
const workspaceVars = Object.keys(workspaceEnvData.workspace)
const personalVars = Object.keys(workspaceEnvData.personal)

if (workspaceVars.length > 0) {
envVarGroups.push({ label: 'Workspace', variables: workspaceVars })
}
if (personalVars.length > 0) {
envVarGroups.push({ label: 'Personal', variables: personalVars })
}
envVarGroups.push({ label: 'Workspace', variables: workspaceVars })
envVarGroups.push({ label: 'Personal', variables: personalVars })
} else {
// Fallback to user env vars only
if (userEnvVars.length > 0) {
envVarGroups.push({ label: 'Personal', variables: userEnvVars })
}
}

// Flatten all variables for filtering and selection
const allEnvVars = envVarGroups.flatMap((group) => group.variables)

// Filter env vars based on search term
const filteredEnvVars = allEnvVars.filter((envVar) =>
envVar.toLowerCase().includes(searchTerm.toLowerCase())
)

// Create filtered groups for display
const filteredGroups = envVarGroups
.map((group) => ({
...group,
Expand All @@ -90,49 +79,37 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
}))
.filter((group) => group.variables.length > 0)

// Reset selection when filtered results change
useEffect(() => {
setSelectedIndex(0)
}, [searchTerm])

// Handle environment variable selection
const handleEnvVarSelect = (envVar: string) => {
const textBeforeCursor = inputValue.slice(0, cursorPosition)
const textAfterCursor = inputValue.slice(cursorPosition)

// Find the start of the env var syntax (last '{{' before cursor)
const lastOpenBraces = textBeforeCursor.lastIndexOf('{{')

// Check if we're in a standard env var context (with braces) or direct typing mode
const isStandardEnvVarContext = lastOpenBraces !== -1

if (isStandardEnvVarContext) {
// Standard behavior with {{ }} syntax
const startText = textBeforeCursor.slice(0, lastOpenBraces)

// Find the end of any existing env var syntax after cursor
const closeIndex = textAfterCursor.indexOf('}}')
const endText = closeIndex !== -1 ? textAfterCursor.slice(closeIndex + 2) : textAfterCursor

// Construct the new value with proper env var syntax
const newValue = `${startText}{{${envVar}}}${endText}`
onSelect(newValue)
} else {
// For direct typing mode (API key fields), check if we need to replace existing text
// This handles the case where user has already typed part of a variable name
if (inputValue.trim() !== '') {
// Replace the entire input with the selected env var
onSelect(`{{${envVar}}}`)
} else {
// Empty input, just insert the env var
onSelect(`{{${envVar}}}`)
}
}

onClose?.()
}

// Add and remove keyboard event listener
useEffect(() => {
if (visible) {
const handleKeyboardEvent = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -201,14 +178,14 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
</div>
) : (
<div
className={cn('py-1', maxHeight !== 'none' && 'overflow-y-auto')}
className={cn('py-1', maxHeight !== 'none' && 'allow-scroll max-h-48 overflow-y-auto')}
style={{
maxHeight: maxHeight !== 'none' ? maxHeight : undefined,
scrollbarWidth: maxHeight !== 'none' ? 'thin' : undefined,
}}
>
{filteredGroups.map((group) => (
<div key={group.label}>
{filteredGroups.length > 1 && (
{workspaceId && (
<div className='border-border/50 border-b px-3 py-1 font-medium text-muted-foreground/70 text-xs uppercase tracking-wide'>
{group.label}
</div>
Expand All @@ -226,7 +203,7 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
)}
onMouseEnter={() => setSelectedIndex(globalIndex)}
onMouseDown={(e) => {
e.preventDefault() // Prevent input blur
e.preventDefault()
handleEnvVarSelect(envVar)
}}
>
Expand All @@ -242,21 +219,17 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
)
}

// Helper function to check for '{{' trigger and get search term
export const checkEnvVarTrigger = (
text: string,
cursorPosition: number
): { show: boolean; searchTerm: string } => {
if (cursorPosition >= 2) {
const textBeforeCursor = text.slice(0, cursorPosition)
// Look for {{ pattern followed by optional text
const match = textBeforeCursor.match(/\{\{(\w*)$/)
if (match) {
return { show: true, searchTerm: match[1] }
}

// Also check for exact {{ without any text after it
// This ensures all env vars show when user just types {{
if (textBeforeCursor.endsWith('{{')) {
return { show: true, searchTerm: '' }
}
Expand Down
6 changes: 1 addition & 5 deletions apps/sim/stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,7 @@ export const resetAllStores = () => {
})
useWorkflowStore.getState().clear()
useSubBlockStore.getState().clear()
useEnvironmentStore.setState({
variables: {},
isLoading: false,
error: null,
})
useEnvironmentStore.getState().reset()
useExecutionStore.getState().reset()
useConsoleStore.setState({ entries: [], isOpen: false })
useCopilotStore.setState({ messages: [], isSendingMessage: false, error: null })
Expand Down
Loading