From ecdb133d1b9df05d5b37b082934880977711b8b0 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 24 Feb 2026 19:12:10 -0800 Subject: [PATCH] improvement(creds): bulk paste functionality, save notification, error notif (#3328) * improvement(creds): bulk paste functionality, save notification, error notif * use effect anti patterns * fix add to cursor button * fix(attio): wrap webhook body in data object and include required filter field * fixed and tested attio webhook lifecycle --- apps/sim/app/chat/components/input/input.tsx | 28 +- .../document-tags-modal.tsx | 11 +- .../credentials/credentials-manager.tsx | 554 +++++++++++------- .../components/credentials/credentials.tsx | 4 +- .../workflow-mcp-servers.tsx | 4 +- .../settings-modal/settings-modal.tsx | 11 + apps/sim/lib/webhooks/processor.ts | 6 +- .../lib/webhooks/provider-subscriptions.ts | 19 +- apps/sim/lib/webhooks/utils.server.ts | 43 ++ apps/sim/stores/modals/settings/store.ts | 14 + apps/sim/stores/modals/settings/types.ts | 4 + apps/sim/triggers/attio/utils.ts | 155 ++++- 12 files changed, 591 insertions(+), 262 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..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() { } } - useEffect(() => { - if (createType !== 'oauth') return - if (createOAuthProviderId || oauthConnections.length === 0) return - setCreateOAuthProviderId(oauthConnections[0]?.providerId || '') - }, [createType, createOAuthProviderId, oauthConnections]) + 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(() => { - setCreateError(null) - }, [createOAuthProviderId]) + 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) + } + }, []) 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,86 +1485,44 @@ 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' ? ( - <> -
-
- - Display Name - - - - - - Copy secret ID - -
- setSelectedDisplayNameDraft(event.target.value)} - autoComplete='off' - disabled={!isSelectedAdmin} - /> -
- -
- - Description - -