From 4aead3ec5d9544af07f5eb7cf10c742897003642 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 24 Feb 2026 18:11:07 -0800 Subject: [PATCH 1/5] improvement(creds): bulk paste functionality, save notification, error notif --- apps/sim/app/chat/components/input/input.tsx | 28 +- .../document-tags-modal.tsx | 11 +- .../credentials/credentials-manager.tsx | 505 +++++++++++------- .../components/credentials/credentials.tsx | 4 +- .../workflow-mcp-servers.tsx | 4 +- .../settings-modal/settings-modal.tsx | 11 + apps/sim/stores/modals/settings/store.ts | 14 + apps/sim/stores/modals/settings/types.ts | 4 + 8 files changed, 352 insertions(+), 229 deletions(-) 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..f67cf17d00 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,6 +442,56 @@ 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(() => { + const store = useSettingsModalStore.getState() + if (selectedCredentialId && isDetailsDirty) { + store.setHasUnsavedChanges(true) + store.setOnCloseAttempt(handleCloseAttemptFromModal) + } else { + store.setHasUnsavedChanges(false) + store.setOnCloseAttempt(null) + } + }, [selectedCredentialId, isDetailsDirty, handleCloseAttemptFromModal]) + + useEffect(() => { + return () => { + const store = useSettingsModalStore.getState() + store.setHasUnsavedChanges(false) + store.setOnCloseAttempt(null) + } + }, []) + useEffect(() => { if (createType !== 'oauth') return if (createOAuthProviderId || oauthConnections.length === 0) return @@ -1030,6 +1088,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. + + )} +
+ )}
@@ -1063,6 +1149,7 @@ export function CredentialsManager() { onChange={(event) => setCreateDisplayName(event.target.value)} placeholder='Secret name' autoComplete='off' + data-lpignore='true' className='mt-[6px]' />
@@ -1074,6 +1161,7 @@ export function CredentialsManager() { placeholder='Optional description' maxLength={500} autoComplete='off' + data-lpignore='true' className='mt-[6px] min-h-[80px] resize-none' />
@@ -1113,18 +1201,6 @@ export function CredentialsManager() { />
- {existingOAuthDisplayName && ( -
-
- -

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

-
-
- )}
) : (
@@ -1148,15 +1224,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 +1251,11 @@ export function CredentialsManager() { />
setCreateEnvValue(event.target.value)} + onFocus={() => setIsCreateEnvValueFocused(true)} + onBlur={() => setIsCreateEnvValueFocused(false)} placeholder='Value' autoComplete='new-password' autoCapitalize='none' @@ -1178,6 +1263,11 @@ export function CredentialsManager() { spellCheck={false} data-lpignore='true' data-1p-ignore='true' + style={ + isCreateEnvValueFocused + ? undefined + : ({ WebkitTextSecurity: 'disc' } as React.CSSProperties) + } />
@@ -1225,7 +1315,7 @@ export function CredentialsManager() { />
{ const updated = [...createBulkEntries] @@ -1239,6 +1329,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 +1484,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 - +
+
+ +