diff --git a/apps/sim/app/chat/components/input/input.tsx b/apps/sim/app/chat/components/input/input.tsx index ea41dbb954..5e385d7f3a 100644 --- a/apps/sim/app/chat/components/input/input.tsx +++ b/apps/sim/app/chat/components/input/input.tsx @@ -3,8 +3,8 @@ import type React from 'react' import { useEffect, useRef, useState } from 'react' import { motion } from 'framer-motion' -import { AlertCircle, Paperclip, Send, Square, X } from 'lucide-react' -import { Tooltip } from '@/components/emcn' +import { Paperclip, Send, Square, X } from 'lucide-react' +import { Badge, Tooltip } from '@/components/emcn' import { VoiceInput } from '@/app/chat/components/input/voice-input' const logger = createLogger('ChatInput') @@ -218,24 +218,12 @@ export const ChatInput: React.FC<{
{/* Error Messages */} {uploadErrors.length > 0 && ( -
-
-
- -
-
- File upload error -
-
- {uploadErrors.map((error, idx) => ( -
- {error} -
- ))} -
-
-
-
+
+ {uploadErrors.map((error, idx) => ( + + {error} + + ))}
)} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx index 13c01e2233..5b24a78c2c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react' import { createLogger } from '@sim/logger' import { + Badge, Button, Combobox, DatePicker, @@ -706,12 +707,10 @@ export function DocumentTagsModal({ (def) => def.displayName.toLowerCase() === editTagForm.displayName.toLowerCase() ) && ( -
-

- Maximum tag definitions reached. You can still use existing tag - definitions, but cannot create new ones. -

-
+ + Maximum tag definitions reached. You can still use existing tag definitions, + but cannot create new ones. + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx index 036f280675..dab88c358b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx @@ -2,7 +2,7 @@ import { createElement, useCallback, useEffect, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { AlertTriangle, Check, Copy, Plus, RefreshCw, Search, Share2, X } from 'lucide-react' +import { AlertTriangle, Check, Clipboard, Plus, Search, Share2, X } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, @@ -58,6 +58,7 @@ import { useOAuthConnections, } from '@/hooks/queries/oauth-connections' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' +import { useSettingsModalStore } from '@/stores/modals/settings/store' const logger = createLogger('CredentialsManager') @@ -143,9 +144,7 @@ function getSecretCredentialType( return scope === 'workspace' ? 'env_workspace' : 'env_personal' } -function typeBadgeVariant(type: WorkspaceCredential['type']): 'blue' | 'amber' | 'gray-secondary' { - if (type === 'oauth') return 'blue' - if (type === 'env_workspace') return 'amber' +function typeBadgeVariant(_type: WorkspaceCredential['type']): 'gray-secondary' { return 'gray-secondary' } @@ -176,7 +175,11 @@ function CredentialSkeleton() { ) } -export function CredentialsManager() { +interface CredentialsManagerProps { + onOpenChange?: (open: boolean) => void +} + +export function CredentialsManager({ onOpenChange }: CredentialsManagerProps) { const params = useParams() const workspaceId = (params?.workspaceId as string) || '' @@ -191,6 +194,7 @@ export function CredentialsManager() { const [createDescription, setCreateDescription] = useState('') const [createEnvKey, setCreateEnvKey] = useState('') const [createEnvValue, setCreateEnvValue] = useState('') + const [isCreateEnvValueFocused, setIsCreateEnvValueFocused] = useState(false) const [createOAuthProviderId, setCreateOAuthProviderId] = useState('') const [createSecretInputMode, setCreateSecretInputMode] = useState('single') const [createBulkEntries, setCreateBulkEntries] = useState([]) @@ -205,6 +209,10 @@ export function CredentialsManager() { const [credentialToDelete, setCredentialToDelete] = useState(null) const [showDeleteConfirmDialog, setShowDeleteConfirmDialog] = useState(false) const [deleteError, setDeleteError] = useState(null) + const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false) + const [unsavedChangesAlertSource, setUnsavedChangesAlertSource] = useState< + 'back' | 'modal-close' + >('back') const { data: session } = useSession() const currentUserId = session?.user?.id || '' @@ -434,15 +442,55 @@ export function CredentialsManager() { } } + const handleBackAttempt = useCallback(() => { + if (isDetailsDirty && !isSavingDetails) { + setUnsavedChangesAlertSource('back') + setShowUnsavedChangesAlert(true) + } else { + setSelectedCredentialId(null) + } + }, [isDetailsDirty, isSavingDetails]) + + const handleDiscardChanges = useCallback(() => { + setShowUnsavedChangesAlert(false) + setSelectedEnvValueDraft(selectedEnvCurrentValue) + setSelectedDescriptionDraft(selectedCredential?.description || '') + setSelectedDisplayNameDraft(selectedCredential?.displayName || '') + setSelectedCredentialId(null) + }, [selectedEnvCurrentValue, selectedCredential]) + + const handleDiscardAndClose = useCallback(() => { + setShowUnsavedChangesAlert(false) + useSettingsModalStore.getState().setHasUnsavedChanges(false) + useSettingsModalStore.getState().setOnCloseAttempt(null) + onOpenChange?.(false) + }, [onOpenChange]) + + const handleCloseAttemptFromModal = useCallback(() => { + if (selectedCredentialId && isDetailsDirty && !isSavingDetails) { + setUnsavedChangesAlertSource('modal-close') + setShowUnsavedChangesAlert(true) + } + }, [selectedCredentialId, isDetailsDirty, isSavingDetails]) + useEffect(() => { - if (createType !== 'oauth') return - if (createOAuthProviderId || oauthConnections.length === 0) return - setCreateOAuthProviderId(oauthConnections[0]?.providerId || '') - }, [createType, createOAuthProviderId, oauthConnections]) + const store = useSettingsModalStore.getState() + if (selectedCredentialId && isDetailsDirty) { + store.setHasUnsavedChanges(true) + store.setOnCloseAttempt(handleCloseAttemptFromModal) + } else { + store.setHasUnsavedChanges(false) + store.setOnCloseAttempt(null) + } + }, [selectedCredentialId, isDetailsDirty, handleCloseAttemptFromModal]) useEffect(() => { - setCreateError(null) - }, [createOAuthProviderId]) + return () => { + const store = useSettingsModalStore.getState() + store.setHasUnsavedChanges(false) + store.setOnCloseAttempt(null) + } + }, []) const applyPendingCredentialCreateRequest = useCallback( (request: PendingCredentialCreateRequest) => { @@ -1030,6 +1078,34 @@ export function CredentialsManager() { Create Secret + {(createError || + existingOAuthDisplayName || + selectedExistingEnvCredential || + crossScopeEnvConflict) && ( +
+ {createError && ( + + {createError} + + )} + {existingOAuthDisplayName && ( + + A secret named "{existingOAuthDisplayName.displayName}" already exists. + + )} + {selectedExistingEnvCredential && ( + + A secret with key "{selectedExistingEnvCredential.displayName}" already exists. + + )} + {!selectedExistingEnvCredential && crossScopeEnvConflict && ( + + A workspace secret with key "{crossScopeEnvConflict.envKey}" already exists. + Workspace secrets take precedence at runtime. + + )} +
+ )}
@@ -1044,8 +1120,16 @@ export function CredentialsManager() { } selectedValue={createType} onChange={(value) => { - setCreateType(value as CreateCredentialType) + const newType = value as CreateCredentialType + setCreateType(newType) setCreateError(null) + if ( + newType === 'oauth' && + !createOAuthProviderId && + oauthConnections.length > 0 + ) { + setCreateOAuthProviderId(oauthConnections[0]?.providerId || '') + } }} placeholder='Select type' /> @@ -1063,6 +1147,7 @@ export function CredentialsManager() { onChange={(event) => setCreateDisplayName(event.target.value)} placeholder='Secret name' autoComplete='off' + data-lpignore='true' className='mt-[6px]' />
@@ -1074,6 +1159,7 @@ export function CredentialsManager() { placeholder='Optional description' maxLength={500} autoComplete='off' + data-lpignore='true' className='mt-[6px] min-h-[80px] resize-none' />
@@ -1087,7 +1173,10 @@ export function CredentialsManager() { ?.label || '' } selectedValue={createOAuthProviderId} - onChange={setCreateOAuthProviderId} + onChange={(value) => { + setCreateOAuthProviderId(value) + setCreateError(null) + }} placeholder='Select OAuth service' searchable searchPlaceholder='Search services...' @@ -1113,18 +1202,6 @@ export function CredentialsManager() { />
- {existingOAuthDisplayName && ( -
-
- -

- A secret named{' '} - {existingOAuthDisplayName.displayName}{' '} - already exists. -

-
-
- )}
) : (
@@ -1148,15 +1225,22 @@ export function CredentialsManager() { }} onPaste={(event) => { const pasted = event.clipboardData.getData('text') - if (pasted.includes('=') && pasted.includes('\n')) { - event.preventDefault() - const { entries } = parseEnvText(pasted) - if (entries.length > 0) { - setCreateSecretInputMode('bulk') - setCreateBulkEntries(entries) - setCreateError(null) - } + const { entries } = parseEnvText(pasted) + if (entries.length === 0) { + return } + + event.preventDefault() + if (entries.length === 1) { + setCreateEnvKey(entries[0].key) + setCreateEnvValue(entries[0].value) + setCreateError(null) + return + } + + setCreateSecretInputMode('bulk') + setCreateBulkEntries(entries) + setCreateError(null) }} placeholder='API_KEY' autoComplete='off' @@ -1168,9 +1252,11 @@ export function CredentialsManager() { />
setCreateEnvValue(event.target.value)} + onFocus={() => setIsCreateEnvValueFocused(true)} + onBlur={() => setIsCreateEnvValueFocused(false)} placeholder='Value' autoComplete='new-password' autoCapitalize='none' @@ -1178,6 +1264,11 @@ export function CredentialsManager() { spellCheck={false} data-lpignore='true' data-1p-ignore='true' + style={ + isCreateEnvValueFocused + ? undefined + : ({ WebkitTextSecurity: 'disc' } as React.CSSProperties) + } />
@@ -1225,7 +1316,7 @@ export function CredentialsManager() { />
{ const updated = [...createBulkEntries] @@ -1239,6 +1330,7 @@ export function CredentialsManager() { spellCheck={false} data-lpignore='true' data-1p-ignore='true' + style={{ WebkitTextSecurity: 'disc' } as React.CSSProperties} />
- - {selectedExistingEnvCredential && ( -
-
- -

- A secret with key{' '} - - {selectedExistingEnvCredential.displayName} - {' '} - already exists. -

-
-
- )} - {!selectedExistingEnvCredential && crossScopeEnvConflict && ( -
-
- -

- A workspace secret with key{' '} - {crossScopeEnvConflict.envKey} already - exists. Workspace secrets take precedence at runtime. -

-
-
- )} - - )} - - {createError && ( -
-
- -

- {createError} -

-
)} @@ -1431,69 +1485,141 @@ export function CredentialsManager() { ) + const unsavedChangesAlertJsx = ( + + + Unsaved Changes + +

+ You have unsaved changes. Are you sure you want to discard them? +

+
+ + + + +
+
+ ) + if (selectedCredential) { return ( <>
-
- Type -
- - {typeLabel(selectedCredential.type)} - - {selectedCredential.role && ( - - {selectedCredential.role} + {selectedCredential.type === 'oauth' ? ( +
+
+
+
+ {selectedOAuthServiceConfig ? ( + createElement(selectedOAuthServiceConfig.icon, { className: 'h-4 w-4' }) + ) : ( + + {resolveProviderLabel(selectedCredential.providerId).slice(0, 1)} + + )} +
+
+

Connected service

+

+ {resolveProviderLabel(selectedCredential.providerId) || 'Unknown service'} +

+
+
+
+ + {typeLabel(selectedCredential.type)} + + {selectedCredential.role && ( + + {selectedCredential.role} + + )} +
+
+
+ ) : ( +
+
+ +
+
+ + {typeLabel(selectedCredential.type)} - )} + {selectedCredential.role && ( + + {selectedCredential.role} + + )} +
-
+ )} {selectedCredential.type === 'oauth' ? ( <> -
-
- +
+
+
setSelectedDisplayNameDraft(event.target.value)} autoComplete='off' + data-lpignore='true' disabled={!isSelectedAdmin} />
-
- - Description - +
+
+ +