diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx
index 8e3862ecc..cc7e92549 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx
@@ -31,10 +31,9 @@ const logger = createLogger('ApiKeys')
interface ApiKeysProps {
onOpenChange?: (open: boolean) => void
- registerCloseHandler?: (handler: (open: boolean) => void) => void
}
-export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
+export function ApiKeys({ onOpenChange }: ApiKeysProps) {
const { data: session } = useSession()
const userId = session?.user?.id
const params = useParams()
@@ -118,12 +117,6 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
onOpenChange?.(open)
}
- useEffect(() => {
- if (registerCloseHandler) {
- registerCloseHandler(handleModalClose)
- }
- }, [registerCloseHandler])
-
useEffect(() => {
if (shouldScrollToBottom && scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
index da5076821..14c83662a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx
@@ -4,8 +4,6 @@ import { CredentialsManager } from '@/app/workspace/[workspaceId]/w/components/s
interface CredentialsProps {
onOpenChange?: (open: boolean) => void
- registerCloseHandler?: (handler: (open: boolean) => void) => void
- registerBeforeLeaveHandler?: (handler: (onProceed: () => void) => void) => void
}
export function Credentials(_props: CredentialsProps) {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
deleted file mode 100644
index eb9081500..000000000
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/environment/environment.tsx
+++ /dev/null
@@ -1,864 +0,0 @@
-'use client'
-
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import { createLogger } from '@sim/logger'
-import { Plus, Search, Share2, Undo2 } from 'lucide-react'
-import { useParams } from 'next/navigation'
-import {
- Button,
- Input as EmcnInput,
- Modal,
- ModalBody,
- ModalContent,
- ModalFooter,
- ModalHeader,
- Tooltip,
-} from '@/components/emcn'
-import { Trash } from '@/components/emcn/icons/trash'
-import { Input, Skeleton } from '@/components/ui'
-import { isValidEnvVarName } from '@/executor/constants'
-import {
- usePersonalEnvironment,
- useRemoveWorkspaceEnvironment,
- useSavePersonalEnvironment,
- useUpsertWorkspaceEnvironment,
- useWorkspaceEnvironment,
- type WorkspaceEnvironmentData,
-} from '@/hooks/queries/environment'
-
-const logger = createLogger('EnvironmentVariables')
-
-const GRID_COLS = 'grid grid-cols-[minmax(0,1fr)_8px_minmax(0,1fr)_auto] items-center'
-
-const generateRowId = (() => {
- let counter = 0
- return () => {
- counter += 1
- return Date.now() + counter
- }
-})()
-
-const createEmptyEnvVar = (): UIEnvironmentVariable => ({
- key: '',
- value: '',
- id: generateRowId(),
-})
-
-interface UIEnvironmentVariable {
- key: string
- value: string
- id?: number
-}
-
-/**
- * Validates an environment variable key.
- * Returns an error message if invalid, undefined if valid.
- */
-function validateEnvVarKey(key: string): string | undefined {
- if (!key) return undefined
- if (key.includes(' ')) return 'Spaces are not allowed'
- if (!isValidEnvVarName(key)) return 'Only letters, numbers, and underscores allowed'
- return undefined
-}
-
-interface EnvironmentVariablesProps {
- registerBeforeLeaveHandler?: (handler: (onProceed: () => void) => void) => void
-}
-
-interface WorkspaceVariableRowProps {
- envKey: string
- value: string
- renamingKey: string | null
- pendingKeyValue: string
- isNewlyPromoted: boolean
- onRenameStart: (key: string) => void
- onPendingKeyChange: (value: string) => void
- onRenameEnd: (key: string, value: string) => void
- onDelete: (key: string) => void
- onDemote: (key: string, value: string) => void
-}
-
-function WorkspaceVariableRow({
- envKey,
- value,
- renamingKey,
- pendingKeyValue,
- isNewlyPromoted,
- onRenameStart,
- onPendingKeyChange,
- onRenameEnd,
- onDelete,
- onDemote,
-}: WorkspaceVariableRowProps) {
- return (
-
-
{
- if (renamingKey !== envKey) onRenameStart(envKey)
- onPendingKeyChange(e.target.value)
- }}
- onBlur={() => onRenameEnd(envKey, value)}
- name={`workspace_env_key_${envKey}_${Math.random()}`}
- autoComplete='off'
- autoCapitalize='off'
- spellCheck='false'
- readOnly
- onFocus={(e) => e.target.removeAttribute('readOnly')}
- className='h-9'
- />
-
-
-
- {isNewlyPromoted && (
-
-
- onDemote(envKey, value)} className='h-9 w-9'>
-
-
-
- Change to personal scope
-
- )}
-
-
- onDelete(envKey)} className='h-9 w-9'>
-
-
-
- Delete secret
-
-
-
- )
-}
-
-export function EnvironmentVariables({ registerBeforeLeaveHandler }: EnvironmentVariablesProps) {
- const params = useParams()
- const workspaceId = (params?.workspaceId as string) || ''
-
- const { data: personalEnvData, isLoading: isPersonalLoading } = usePersonalEnvironment()
- const { data: workspaceEnvData, isLoading: isWorkspaceLoading } = useWorkspaceEnvironment(
- workspaceId,
- {
- select: useCallback(
- (data: WorkspaceEnvironmentData): WorkspaceEnvironmentData => ({
- workspace: data.workspace || {},
- personal: data.personal || {},
- conflicts: data.conflicts || [],
- }),
- []
- ),
- }
- )
- const savePersonalMutation = useSavePersonalEnvironment()
- const upsertWorkspaceMutation = useUpsertWorkspaceEnvironment()
- const removeWorkspaceMutation = useRemoveWorkspaceEnvironment()
-
- const isLoading = isPersonalLoading || isWorkspaceLoading
- const variables = useMemo(() => personalEnvData || {}, [personalEnvData])
-
- const [envVars, setEnvVars] = useState([])
- const [searchTerm, setSearchTerm] = useState('')
- const [focusedValueIndex, setFocusedValueIndex] = useState(null)
- const [showUnsavedChanges, setShowUnsavedChanges] = useState(false)
- const [shouldScrollToBottom, setShouldScrollToBottom] = useState(false)
- const [workspaceVars, setWorkspaceVars] = useState>({})
- const [conflicts, setConflicts] = useState([])
- const [renamingKey, setRenamingKey] = useState(null)
- const [pendingKeyValue, setPendingKeyValue] = useState('')
- const [changeToken, setChangeToken] = useState(0)
-
- const initialWorkspaceVarsRef = useRef>({})
- const scrollContainerRef = useRef(null)
- const pendingProceedCallback = useRef<(() => void) | null>(null)
- const initialVarsRef = useRef([])
- const hasChangesRef = useRef(false)
- const hasSavedRef = useRef(false)
-
- const filteredEnvVars = useMemo(() => {
- const mapped = envVars.map((envVar, index) => ({ envVar, originalIndex: index }))
- if (!searchTerm.trim()) return mapped
- const term = searchTerm.toLowerCase()
- return mapped.filter(({ envVar }) => envVar.key.toLowerCase().includes(term))
- }, [envVars, searchTerm])
-
- const filteredWorkspaceEntries = useMemo(() => {
- const entries = Object.entries(workspaceVars)
- if (!searchTerm.trim()) return entries
- const term = searchTerm.toLowerCase()
- return entries.filter(([key]) => key.toLowerCase().includes(term))
- }, [workspaceVars, searchTerm])
-
- const hasChanges = useMemo(() => {
- const initialVars = initialVarsRef.current.filter((v) => v.key || v.value)
- const currentVars = envVars.filter((v) => v.key || v.value)
- const initialMap = new Map(initialVars.map((v) => [v.key, v.value]))
- const currentMap = new Map(currentVars.map((v) => [v.key, v.value]))
-
- if (initialMap.size !== currentMap.size) return true
-
- for (const [key, value] of currentMap) {
- if (initialMap.get(key) !== value) return true
- }
-
- for (const key of initialMap.keys()) {
- if (!currentMap.has(key)) return true
- }
-
- const before = initialWorkspaceVarsRef.current
- const after = workspaceVars
- const allKeys = new Set([...Object.keys(before), ...Object.keys(after)])
-
- if (Object.keys(before).length !== Object.keys(after).length) return true
-
- for (const key of allKeys) {
- if (before[key] !== after[key]) return true
- }
-
- return false
- }, [envVars, workspaceVars, changeToken])
-
- const hasConflicts = useMemo(() => {
- return envVars.some((envVar) => !!envVar.key && Object.hasOwn(workspaceVars, envVar.key))
- }, [envVars, workspaceVars])
-
- const hasInvalidKeys = useMemo(() => {
- return envVars.some((envVar) => !!envVar.key && validateEnvVarKey(envVar.key))
- }, [envVars])
-
- useEffect(() => {
- hasChangesRef.current = hasChanges
- }, [hasChanges])
-
- const handleBeforeLeave = useCallback((onProceed: () => void) => {
- if (hasChangesRef.current) {
- setShowUnsavedChanges(true)
- pendingProceedCallback.current = onProceed
- } else {
- onProceed()
- }
- }, [])
-
- useEffect(() => {
- if (hasSavedRef.current) return
-
- const existingVars = Object.values(variables)
- const initialVars = existingVars.length
- ? existingVars.map((envVar) => ({
- ...envVar,
- id: generateRowId(),
- }))
- : [createEmptyEnvVar()]
- initialVarsRef.current = JSON.parse(JSON.stringify(initialVars))
- setEnvVars(JSON.parse(JSON.stringify(initialVars)))
- pendingProceedCallback.current = null
- }, [variables])
-
- useEffect(() => {
- if (workspaceEnvData) {
- if (hasSavedRef.current) {
- setConflicts(workspaceEnvData?.conflicts || [])
- hasSavedRef.current = false
- } else {
- setWorkspaceVars(workspaceEnvData?.workspace || {})
- initialWorkspaceVarsRef.current = workspaceEnvData?.workspace || {}
- setConflicts(workspaceEnvData?.conflicts || [])
- }
- }
- }, [workspaceEnvData])
-
- useEffect(() => {
- if (registerBeforeLeaveHandler) {
- registerBeforeLeaveHandler(handleBeforeLeave)
- }
- }, [registerBeforeLeaveHandler, handleBeforeLeave])
-
- useEffect(() => {
- if (shouldScrollToBottom && scrollContainerRef.current) {
- scrollContainerRef.current.scrollTo({
- top: scrollContainerRef.current.scrollHeight,
- behavior: 'smooth',
- })
- setShouldScrollToBottom(false)
- }
- }, [shouldScrollToBottom])
-
- useEffect(() => {
- const personalKeys = envVars.map((envVar) => envVar.key.trim()).filter((key) => key.length > 0)
-
- const uniquePersonalKeys = Array.from(new Set(personalKeys))
-
- const computedConflicts = uniquePersonalKeys.filter((key) => Object.hasOwn(workspaceVars, key))
-
- setConflicts((prev) => {
- if (prev.length === computedConflicts.length) {
- const sameKeys = prev.every((key) => computedConflicts.includes(key))
- if (sameKeys) return prev
- }
- return computedConflicts
- })
- }, [envVars, workspaceVars])
-
- const handleWorkspaceKeyRename = useCallback(
- (currentKey: string, currentValue: string) => {
- const newKey = pendingKeyValue.trim()
- if (!renamingKey || renamingKey !== currentKey) return
- setRenamingKey(null)
- if (!newKey || newKey === currentKey) return
-
- setWorkspaceVars((prev) => {
- const next = { ...prev }
- delete next[currentKey]
- next[newKey] = currentValue
- return next
- })
- },
- [pendingKeyValue, renamingKey]
- )
-
- const handleDeleteWorkspaceVar = useCallback((key: string) => {
- setWorkspaceVars((prev) => {
- const next = { ...prev }
- delete next[key]
- return next
- })
- }, [])
-
- const addEnvVar = useCallback(() => {
- setEnvVars((prev) => [...prev, createEmptyEnvVar()])
- setSearchTerm('')
- setShouldScrollToBottom(true)
- }, [])
-
- const updateEnvVar = useCallback((index: number, field: 'key' | 'value', value: string) => {
- setEnvVars((prev) => {
- const newEnvVars = [...prev]
- newEnvVars[index][field] = value
- return newEnvVars
- })
- }, [])
-
- const removeEnvVar = useCallback((index: number) => {
- setEnvVars((prev) => {
- const newEnvVars = prev.filter((_, i) => i !== index)
- return newEnvVars.length ? newEnvVars : [createEmptyEnvVar()]
- })
- }, [])
-
- const handleValueFocus = useCallback((index: number, e: React.FocusEvent) => {
- setFocusedValueIndex(index)
- e.target.scrollLeft = 0
- }, [])
-
- const handleValueClick = useCallback((e: React.MouseEvent) => {
- e.preventDefault()
- e.currentTarget.scrollLeft = 0
- }, [])
-
- const parseEnvVarLine = useCallback((line: string): UIEnvironmentVariable | null => {
- const trimmed = line.trim()
-
- if (!trimmed || trimmed.startsWith('#')) return null
-
- const withoutExport = trimmed.replace(/^export\s+/, '')
-
- const equalIndex = withoutExport.indexOf('=')
- if (equalIndex === -1 || equalIndex === 0) return null
-
- const potentialKey = withoutExport.substring(0, equalIndex).trim()
- if (!isValidEnvVarName(potentialKey)) return null
-
- let value = withoutExport.substring(equalIndex + 1)
-
- const looksLikeBase64Key = /^[A-Za-z0-9+/]+$/.test(potentialKey) && !potentialKey.includes('_')
- const valueIsJustPadding = /^=+$/.test(value.trim())
- if (looksLikeBase64Key && valueIsJustPadding && potentialKey.length > 20) {
- return null
- }
-
- const trimmedValue = value.trim()
- if (
- !trimmedValue.startsWith('"') &&
- !trimmedValue.startsWith("'") &&
- !trimmedValue.startsWith('`')
- ) {
- const commentIndex = value.search(/\s#/)
- if (commentIndex !== -1) {
- value = value.substring(0, commentIndex)
- }
- }
-
- value = value.trim()
-
- if (
- (value.startsWith('"') && value.endsWith('"')) ||
- (value.startsWith("'") && value.endsWith("'")) ||
- (value.startsWith('`') && value.endsWith('`'))
- ) {
- value = value.slice(1, -1)
- }
-
- return { key: potentialKey, value, id: generateRowId() }
- }, [])
-
- const handleSingleValuePaste = useCallback(
- (text: string, index: number, inputType: 'key' | 'value') => {
- setEnvVars((prev) => {
- const newEnvVars = [...prev]
- newEnvVars[index][inputType] = text
- return newEnvVars
- })
- },
- []
- )
-
- const handleKeyValuePaste = useCallback(
- (lines: string[]) => {
- const parsedVars = lines
- .map(parseEnvVarLine)
- .filter((parsed): parsed is UIEnvironmentVariable => parsed !== null)
- .filter(({ key, value }) => key && value)
-
- if (parsedVars.length > 0) {
- setEnvVars((prev) => {
- const existingVars = prev.filter((v) => v.key || v.value)
- return [...existingVars, ...parsedVars]
- })
- setShouldScrollToBottom(true)
- }
- },
- [parseEnvVarLine]
- )
-
- const handlePaste = useCallback(
- (e: React.ClipboardEvent, index: number) => {
- const text = e.clipboardData.getData('text').trim()
- if (!text) return
-
- const lines = text.split('\n').filter((line) => line.trim())
- if (lines.length === 0) return
-
- e.preventDefault()
-
- const inputType = (e.target as HTMLInputElement).getAttribute('data-input-type') as
- | 'key'
- | 'value'
-
- if (inputType) {
- const hasValidEnvVarPattern = lines.some((line) => parseEnvVarLine(line) !== null)
- if (!hasValidEnvVarPattern) {
- handleSingleValuePaste(text, index, inputType)
- return
- }
- }
-
- handleKeyValuePaste(lines)
- },
- [parseEnvVarLine, handleSingleValuePaste, handleKeyValuePaste]
- )
-
- const handleCancel = useCallback(() => {
- setEnvVars(JSON.parse(JSON.stringify(initialVarsRef.current)))
- setWorkspaceVars({ ...initialWorkspaceVarsRef.current })
- setShowUnsavedChanges(false)
-
- pendingProceedCallback.current?.()
- pendingProceedCallback.current = null
- }, [])
-
- const handleSave = useCallback(async () => {
- const onProceed = pendingProceedCallback.current
-
- const prevInitialVars = [...initialVarsRef.current]
- const prevInitialWorkspaceVars = { ...initialWorkspaceVarsRef.current }
-
- try {
- setShowUnsavedChanges(false)
- hasSavedRef.current = true
-
- initialWorkspaceVarsRef.current = { ...workspaceVars }
- initialVarsRef.current = JSON.parse(JSON.stringify(envVars.filter((v) => v.key && v.value)))
-
- setChangeToken((prev) => prev + 1)
-
- const validVariables = envVars
- .filter((v) => v.key && v.value)
- .reduce>((acc, { key, value }) => ({ ...acc, [key]: value }), {})
-
- await savePersonalMutation.mutateAsync({ variables: validVariables })
-
- const before = prevInitialWorkspaceVars
- const after = workspaceVars
- const toUpsert: Record = {}
- const toDelete: string[] = []
-
- for (const [k, v] of Object.entries(after)) {
- if (!(k in before) || before[k] !== v) {
- toUpsert[k] = v
- }
- }
-
- for (const k of Object.keys(before)) {
- if (!(k in after)) toDelete.push(k)
- }
-
- if (workspaceId) {
- if (Object.keys(toUpsert).length) {
- await upsertWorkspaceMutation.mutateAsync({ workspaceId, variables: toUpsert })
- }
- if (toDelete.length) {
- await removeWorkspaceMutation.mutateAsync({ workspaceId, keys: toDelete })
- }
- }
-
- onProceed?.()
- pendingProceedCallback.current = null
- } catch (error) {
- hasSavedRef.current = false
- initialVarsRef.current = prevInitialVars
- initialWorkspaceVarsRef.current = prevInitialWorkspaceVars
- logger.error('Failed to save environment variables:', error)
- }
- }, [
- envVars,
- workspaceVars,
- workspaceId,
- savePersonalMutation,
- upsertWorkspaceMutation,
- removeWorkspaceMutation,
- ])
-
- const promoteToWorkspace = useCallback(
- (envVar: UIEnvironmentVariable) => {
- if (!envVar.key || !envVar.value || !workspaceId) return
- setWorkspaceVars((prev) => ({ ...prev, [envVar.key]: envVar.value }))
- setEnvVars((prev) => {
- const filtered = prev.filter((entry) => entry !== envVar)
- return filtered.length ? filtered : [createEmptyEnvVar()]
- })
- },
- [workspaceId]
- )
-
- const demoteToPersonal = useCallback((key: string, value: string) => {
- if (!key) return
- setWorkspaceVars((prev) => {
- const next = { ...prev }
- delete next[key]
- return next
- })
- setEnvVars((prev) => [...prev, { key, value, id: generateRowId() }])
- }, [])
-
- const conflictClassName = 'border-[var(--text-error)] bg-[#F6D2D2] dark:bg-[#442929]'
-
- const renderEnvVarRow = useCallback(
- (envVar: UIEnvironmentVariable, originalIndex: number) => {
- const isConflict = !!envVar.key && Object.hasOwn(workspaceVars, envVar.key)
- const keyError = validateEnvVarKey(envVar.key)
- const maskedValueStyle =
- focusedValueIndex !== originalIndex && !isConflict
- ? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
- : undefined
-
- return (
- <>
-
-
updateEnvVar(originalIndex, 'key', e.target.value)}
- onPaste={(e) => handlePaste(e, originalIndex)}
- placeholder='API_KEY'
- name={`env_variable_name_${envVar.id || originalIndex}_${Math.random()}`}
- autoComplete='off'
- autoCapitalize='off'
- spellCheck='false'
- readOnly
- onFocus={(e) => e.target.removeAttribute('readOnly')}
- className={`h-9 ${isConflict ? conflictClassName : ''} ${keyError ? 'border-[var(--text-error)]' : ''}`}
- />
-
- updateEnvVar(originalIndex, 'value', e.target.value)}
- type='text'
- onFocus={(e) => {
- if (!isConflict) {
- e.target.removeAttribute('readOnly')
- handleValueFocus(originalIndex, e)
- }
- }}
- onClick={handleValueClick}
- onBlur={() => setFocusedValueIndex(null)}
- onPaste={(e) => handlePaste(e, originalIndex)}
- placeholder={isConflict ? 'Workspace override active' : 'Enter value'}
- disabled={isConflict}
- aria-disabled={isConflict}
- name={`env_variable_value_${envVar.id || originalIndex}_${Math.random()}`}
- autoComplete='off'
- autoCapitalize='off'
- spellCheck='false'
- readOnly={isConflict}
- style={maskedValueStyle}
- className={`h-9 ${isConflict ? `cursor-not-allowed ${conflictClassName}` : ''}`}
- />
-
-
-
- promoteToWorkspace(envVar)}
- className='h-9 w-9'
- >
-
-
-
- Change to workspace scope
-
-
-
- removeEnvVar(originalIndex)}
- className='h-9 w-9'
- >
-
-
-
- Delete secret
-
-
-
- {keyError && (
-
- {keyError}
-
- )}
- {isConflict && !keyError && (
-
- Workspace variable with the same name overrides this. Rename your personal key to use
- it.
-
- )}
- >
- )
- },
- [
- workspaceVars,
- workspaceId,
- focusedValueIndex,
- updateEnvVar,
- handlePaste,
- handleValueFocus,
- handleValueClick,
- promoteToWorkspace,
- removeEnvVar,
- ]
- )
-
- return (
- <>
-
-
-
-
-
-
-
-
-
- setSearchTerm(e.target.value)}
- name='env_search_field'
- autoComplete='off'
- autoCapitalize='off'
- spellCheck='false'
- readOnly
- onFocus={(e) => e.target.removeAttribute('readOnly')}
- className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
- />
-
-
-
- Add
-
-
-
-
- Save
-
-
- {hasConflicts && Resolve all conflicts before saving }
- {hasInvalidKeys && !hasConflicts && (
- Fix invalid variable names before saving
- )}
-
-
-
-
-
- {isLoading ? (
- <>
-
-
-
- {Array.from({ length: 2 }, (_, i) => (
-
- ))}
-
- >
- ) : (
- <>
- {(!searchTerm.trim() || filteredWorkspaceEntries.length > 0) && (
-
-
- Workspace
-
- {!searchTerm.trim() && Object.keys(workspaceVars).length === 0 ? (
-
- No workspace variables yet
-
- ) : (
- (searchTerm.trim()
- ? filteredWorkspaceEntries
- : Object.entries(workspaceVars)
- ).map(([key, value]) => (
-
- ))
- )}
-
- )}
-
- {(!searchTerm.trim() || filteredEnvVars.length > 0) && (
-
-
- Personal
-
- {filteredEnvVars.map(({ envVar, originalIndex }) => (
-
- {renderEnvVarRow(envVar, originalIndex)}
-
- ))}
-
- )}
- {searchTerm.trim() &&
- filteredEnvVars.length === 0 &&
- filteredWorkspaceEntries.length === 0 &&
- (envVars.length > 0 || Object.keys(workspaceVars).length > 0) && (
-
- No secrets found matching "{searchTerm}"
-
- )}
- >
- )}
-
-
-
-
-
-
- Unsaved Changes
-
-
- {hasConflicts || hasInvalidKeys
- ? `You have unsaved changes, but ${hasConflicts ? 'conflicts must be resolved' : 'invalid variable names must be fixed'} before saving. You can discard your changes to close the modal.`
- : 'You have unsaved changes. Do you want to save them before closing?'}
-
-
-
-
- Discard Changes
-
- {hasConflicts || hasInvalidKeys ? (
-
-
-
- Save Changes
-
-
-
- {hasConflicts
- ? 'Resolve all conflicts before saving'
- : 'Fix invalid variable names before saving'}
-
-
- ) : (
-
- Save Changes
-
- )}
-
-
-
- >
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts
index 0621308ac..c19f269fe 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts
@@ -5,10 +5,8 @@ export { CredentialSets } from './credential-sets/credential-sets'
export { Credentials } from './credentials/credentials'
export { CustomTools } from './custom-tools/custom-tools'
export { Debug } from './debug/debug'
-export { EnvironmentVariables } from './environment/environment'
export { Files as FileUploads } from './files/files'
export { General } from './general/general'
-export { Integrations } from './integrations/integrations'
export { MCP } from './mcp/mcp'
export { Skills } from './skills/skills'
export { Subscription } from './subscription/subscription'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/integrations/integrations.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/integrations/integrations.tsx
deleted file mode 100644
index dabdfc03f..000000000
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/integrations/integrations.tsx
+++ /dev/null
@@ -1,417 +0,0 @@
-'use client'
-
-import { createElement, useEffect, useRef, useState } from 'react'
-import { createLogger } from '@sim/logger'
-import { Check, ChevronDown, ExternalLink, Search } from 'lucide-react'
-import { useRouter, useSearchParams } from 'next/navigation'
-import {
- Button,
- Label,
- Modal,
- ModalBody,
- ModalContent,
- ModalFooter,
- ModalHeader,
-} from '@/components/emcn'
-import { Input, Skeleton } from '@/components/ui'
-import { cn } from '@/lib/core/utils/cn'
-import { OAUTH_PROVIDERS } from '@/lib/oauth'
-import {
- type ServiceInfo,
- useConnectOAuthService,
- useDisconnectOAuthService,
- useOAuthConnections,
-} from '@/hooks/queries/oauth-connections'
-import { usePermissionConfig } from '@/hooks/use-permission-config'
-
-const logger = createLogger('Integrations')
-
-/**
- * Static skeleton structure matching OAUTH_PROVIDERS layout
- * Each entry: [providerName, serviceCount]
- */
-const SKELETON_STRUCTURE: [string, number][] = [
- ['Google', 7],
- ['Microsoft', 6],
- ['GitHub', 1],
- ['X', 1],
- ['Confluence', 1],
- ['Jira', 1],
- ['Airtable', 1],
- ['Notion', 1],
- ['Linear', 1],
- ['Slack', 1],
- ['Reddit', 1],
- ['Wealthbox', 1],
- ['Webflow', 1],
- ['Trello', 1],
- ['Asana', 1],
- ['Pipedrive', 1],
- ['HubSpot', 1],
- ['Salesforce', 1],
-]
-
-function IntegrationsSkeleton() {
- return (
-
-
-
-
-
-
-
-
- {SKELETON_STRUCTURE.map(([providerName, serviceCount]) => (
-
-
- {Array.from({ length: serviceCount }).map((_, index) => (
-
- ))}
-
- ))}
-
-
-
- )
-}
-
-interface IntegrationsProps {
- onOpenChange?: (open: boolean) => void
- registerCloseHandler?: (handler: (open: boolean) => void) => void
-}
-
-export function Integrations({ onOpenChange, registerCloseHandler }: IntegrationsProps) {
- const router = useRouter()
- const searchParams = useSearchParams()
- const pendingServiceRef = useRef(null)
-
- const { data: services = [], isPending } = useOAuthConnections()
- const connectService = useConnectOAuthService()
- const disconnectService = useDisconnectOAuthService()
- const { config: permissionConfig } = usePermissionConfig()
-
- const [searchTerm, setSearchTerm] = useState('')
- const [pendingService, setPendingService] = useState(null)
- const [authSuccess, setAuthSuccess] = useState(false)
- const [showActionRequired, setShowActionRequired] = useState(false)
- const prevConnectedIdsRef = useRef>(new Set())
- const connectionAddedRef = useRef(false)
-
- // Disconnect confirmation dialog state
- const [showDisconnectDialog, setShowDisconnectDialog] = useState(false)
- const [serviceToDisconnect, setServiceToDisconnect] = useState<{
- service: ServiceInfo
- accountId: string
- } | null>(null)
-
- // Check for OAuth callback - just show success message
- useEffect(() => {
- const code = searchParams.get('code')
- const state = searchParams.get('state')
- const error = searchParams.get('error')
-
- if (code && state) {
- logger.info('OAuth callback successful')
- setAuthSuccess(true)
-
- // Clear URL parameters without changing the page
- const url = new URL(window.location.href)
- url.searchParams.delete('code')
- url.searchParams.delete('state')
- router.replace(url.pathname + url.search)
- } else if (error) {
- logger.error('OAuth error:', { error })
- }
- }, [searchParams, router])
-
- // Track when a new connection is added compared to previous render
- useEffect(() => {
- try {
- const currentConnected = new Set()
- services.forEach((svc) => {
- if (svc.isConnected) currentConnected.add(svc.id)
- })
- // Detect new connections by comparing to previous connected set
- for (const id of currentConnected) {
- if (!prevConnectedIdsRef.current.has(id)) {
- connectionAddedRef.current = true
- break
- }
- }
- prevConnectedIdsRef.current = currentConnected
- } catch {}
- }, [services])
-
- // On mount, register a close handler so the parent modal can delegate close events here
- useEffect(() => {
- if (!registerCloseHandler) return
- const handle = (open: boolean) => {
- if (open) return
- try {
- if (typeof window !== 'undefined') {
- window.dispatchEvent(
- new CustomEvent('oauth-integration-closed', {
- detail: { success: connectionAddedRef.current === true },
- })
- )
- }
- } catch {}
- onOpenChange?.(open)
- }
- registerCloseHandler(handle)
- }, [registerCloseHandler, onOpenChange])
-
- // Handle connect button click
- const handleConnect = async (service: ServiceInfo) => {
- try {
- logger.info('Connecting service:', {
- serviceId: service.id,
- providerId: service.providerId,
- scopes: service.scopes,
- })
-
- // better-auth will automatically redirect back to this URL after OAuth
- await connectService.mutateAsync({
- providerId: service.providerId,
- callbackURL: window.location.href,
- })
- } catch (error) {
- logger.error('OAuth connection error:', { error })
- }
- }
-
- /**
- * Opens the disconnect confirmation dialog for a service.
- */
- const handleDisconnect = (service: ServiceInfo, accountId: string) => {
- setServiceToDisconnect({ service, accountId })
- setShowDisconnectDialog(true)
- }
-
- /**
- * Confirms and executes the service disconnection.
- */
- const confirmDisconnect = async () => {
- if (!serviceToDisconnect) return
-
- setShowDisconnectDialog(false)
- const { service, accountId } = serviceToDisconnect
- setServiceToDisconnect(null)
-
- try {
- await disconnectService.mutateAsync({
- provider: service.providerId.split('-')[0],
- providerId: service.providerId,
- serviceId: service.id,
- accountId,
- })
- } catch (error) {
- logger.error('Error disconnecting service:', { error })
- }
- }
-
- // Group services by provider, filtering by permission config
- const groupedServices = services.reduce(
- (acc, service) => {
- // Filter based on allowedIntegrations
- if (
- permissionConfig.allowedIntegrations !== null &&
- !permissionConfig.allowedIntegrations.includes(service.id)
- ) {
- return acc
- }
-
- // Find the provider for this service
- const providerKey =
- Object.keys(OAUTH_PROVIDERS).find((key) =>
- Object.keys(OAUTH_PROVIDERS[key].services).includes(service.id)
- ) || 'other'
-
- if (!acc[providerKey]) {
- acc[providerKey] = []
- }
-
- acc[providerKey].push(service)
- return acc
- },
- {} as Record
- )
-
- // Filter services based on search term
- const filteredGroupedServices = Object.entries(groupedServices).reduce(
- (acc, [providerKey, providerServices]) => {
- const filteredServices = providerServices.filter(
- (service) =>
- service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- service.description.toLowerCase().includes(searchTerm.toLowerCase())
- )
-
- if (filteredServices.length > 0) {
- acc[providerKey] = filteredServices
- }
-
- return acc
- },
- {} as Record
- )
-
- const scrollToHighlightedService = () => {
- if (pendingServiceRef.current) {
- pendingServiceRef.current.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- })
- }
- }
-
- if (isPending) {
- return
- }
-
- return (
- <>
-
-
-
- setSearchTerm(e.target.value)}
- className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
- />
-
-
-
-
- {authSuccess && (
-
-
-
- Account connected successfully!
-
-
- )}
-
- {pendingService && showActionRequired && (
-
-
-
-
- Action Required: {' '}
- Please connect your account to enable the requested features.
-
-
- Go to service
-
-
-
-
- )}
-
-
- {Object.entries(filteredGroupedServices).map(([providerKey, providerServices]) => (
-
-
- {OAUTH_PROVIDERS[providerKey]?.name || 'Other Services'}
-
- {providerServices.map((service) => (
-
-
-
- {createElement(service.icon, { className: 'h-4 w-4' })}
-
-
-
{service.name}
- {service.accounts && service.accounts.length > 0 ? (
-
- {service.accounts.map((a) => a.name).join(', ')}
-
- ) : (
-
- {service.description}
-
- )}
-
-
-
- {service.accounts && service.accounts.length > 0 ? (
-
handleDisconnect(service, service.accounts![0].id)}
- disabled={disconnectService.isPending}
- >
- Disconnect
-
- ) : (
-
handleConnect(service)}
- disabled={connectService.isPending}
- >
- Connect
-
- )}
-
- ))}
-
- ))}
-
- {searchTerm.trim() && Object.keys(filteredGroupedServices).length === 0 && (
-
- No services found matching "{searchTerm}"
-
- )}
-
-
-
-
-
-
-
- Disconnect Service
-
-
- Are you sure you want to disconnect{' '}
-
- {serviceToDisconnect?.service.name}
-
- ?{' '}
-
- This will revoke access and you will need to reconnect to use this service.
-
-
-
-
- setShowDisconnectDialog(false)}>
- Cancel
-
-
- Disconnect
-
-
-
-
- >
- )
-}
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 ff8781c17..2222b2690 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
@@ -1,6 +1,6 @@
'use client'
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
import { useQueryClient } from '@tanstack/react-query'
@@ -79,9 +79,7 @@ interface SettingsModalProps {
type SettingsSection =
| 'general'
| 'credentials'
- | 'environment'
| 'template-profile'
- | 'integrations'
| 'credential-sets'
| 'access-control'
| 'apikeys'
@@ -216,8 +214,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const activeOrganization = organizationsData?.activeOrganization
const { config: permissionConfig } = usePermissionConfig()
- const environmentBeforeLeaveHandler = useRef<((onProceed: () => void) => void) | null>(null)
- const integrationsCloseHandler = useRef<((open: boolean) => void) | null>(null)
const userEmail = session?.user?.email
const userId = session?.user?.id
@@ -319,32 +315,12 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
if (!isBillingEnabled && (activeSection === 'subscription' || activeSection === 'team')) {
return 'general'
}
- if (activeSection === 'environment' || activeSection === 'integrations') {
- return 'credentials'
- }
return activeSection
}, [activeSection])
- const registerEnvironmentBeforeLeaveHandler = useCallback(
- (handler: (onProceed: () => void) => void) => {
- environmentBeforeLeaveHandler.current = handler
- },
- []
- )
-
- const registerIntegrationsCloseHandler = useCallback((handler: (open: boolean) => void) => {
- integrationsCloseHandler.current = handler
- }, [])
-
const handleSectionChange = useCallback(
(sectionId: SettingsSection) => {
if (sectionId === effectiveActiveSection) return
-
- if (effectiveActiveSection === 'credentials' && environmentBeforeLeaveHandler.current) {
- environmentBeforeLeaveHandler.current(() => setActiveSection(sectionId))
- return
- }
-
setActiveSection(sectionId)
},
[effectiveActiveSection]
@@ -368,11 +344,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
useEffect(() => {
const handleOpenSettings = (event: CustomEvent<{ tab: SettingsSection }>) => {
- if (event.detail.tab === 'environment' || event.detail.tab === 'integrations') {
- setActiveSection('credentials')
- } else {
- setActiveSection(event.detail.tab)
- }
+ setActiveSection(event.detail.tab)
onOpenChange(true)
}
@@ -477,29 +449,8 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
}
}
- // Handle dialog close - delegate to environment component if it's active
const handleDialogOpenChange = (newOpen: boolean) => {
- if (
- !newOpen &&
- effectiveActiveSection === 'credentials' &&
- environmentBeforeLeaveHandler.current
- ) {
- environmentBeforeLeaveHandler.current(() => {
- if (integrationsCloseHandler.current) {
- integrationsCloseHandler.current(newOpen)
- } else {
- onOpenChange(false)
- }
- })
- } else if (
- !newOpen &&
- effectiveActiveSection === 'credentials' &&
- integrationsCloseHandler.current
- ) {
- integrationsCloseHandler.current(newOpen)
- } else {
- onOpenChange(newOpen)
- }
+ onOpenChange(newOpen)
}
return (
@@ -548,11 +499,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
{effectiveActiveSection === 'general' && }
{effectiveActiveSection === 'credentials' && (
-
+
)}
{effectiveActiveSection === 'template-profile' && }
{effectiveActiveSection === 'credential-sets' && }
diff --git a/apps/sim/stores/modals/settings/types.ts b/apps/sim/stores/modals/settings/types.ts
index e010c6fa3..247e2c099 100644
--- a/apps/sim/stores/modals/settings/types.ts
+++ b/apps/sim/stores/modals/settings/types.ts
@@ -1,9 +1,7 @@
export type SettingsSection =
| 'general'
| 'credentials'
- | 'environment'
| 'template-profile'
- | 'integrations'
| 'apikeys'
| 'files'
| 'subscription'