diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx index 21ae97ca049..f35949a382a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx @@ -323,7 +323,6 @@ export function CredentialsManager() { const [selectedDescriptionDraft, setSelectedDescriptionDraft] = useState('') const [copyIdSuccess, setCopyIdSuccess] = useState(false) const [detailsError, setDetailsError] = useState(null) - const [isSavingDetails, setIsSavingDetails] = useState(false) const [showDetailUnsavedChanges, setShowDetailUnsavedChanges] = useState(false) const [memberUserId, setMemberUserId] = useState('') const [memberRole, setMemberRole] = useState('member') @@ -350,8 +349,9 @@ export function CredentialsManager() { [envCredentials, selectedCredentialId] ) - if (selectedCredential?.id !== prevSelectedCredentialId) { - setPrevSelectedCredentialId(selectedCredential?.id ?? null) + const currentCredentialId = selectedCredential?.id ?? null + if (currentCredentialId !== prevSelectedCredentialId) { + setPrevSelectedCredentialId(currentCredentialId) if (!selectedCredential) { setSelectedDescriptionDraft('') setSelectedDisplayNameDraft('') @@ -474,6 +474,11 @@ export function CredentialsManager() { return personalInvalid || workspaceInvalid }, [envVars, newWorkspaceRows]) + const isListSaving = + savePersonalMutation.isPending || + upsertWorkspaceMutation.isPending || + removeWorkspaceMutation.isPending + hasChangesRef.current = hasChanges shouldBlockNavRef.current = hasChanges || isDetailsDirty @@ -652,12 +657,12 @@ export function CredentialsManager() { ) const handleBackAttempt = useCallback(() => { - if (isDetailsDirty && !isSavingDetails) { + if (isDetailsDirty && !updateCredential.isPending) { setShowDetailUnsavedChanges(true) } else { setSelectedCredentialId(null) } - }, [isDetailsDirty, isSavingDetails]) + }, [isDetailsDirty, updateCredential.isPending]) const handleDiscardDetailChanges = useCallback(() => { setShowDetailUnsavedChanges(false) @@ -667,9 +672,9 @@ export function CredentialsManager() { }, [selectedCredential]) const handleSaveDetails = useCallback(async () => { - if (!selectedCredential || !isSelectedAdmin || !isDetailsDirty) return + if (!selectedCredential || !isSelectedAdmin || !isDetailsDirty || updateCredential.isPending) + return setDetailsError(null) - setIsSavingDetails(true) try { if (isDisplayNameDirty || isDescriptionDirty) { @@ -683,8 +688,6 @@ export function CredentialsManager() { const message = error instanceof Error ? error.message : 'Failed to save changes' setDetailsError(message) logger.error('Failed to save secret details', error) - } finally { - setIsSavingDetails(false) } }, [ selectedCredential, @@ -906,6 +909,8 @@ export function CredentialsManager() { const handleCancel = resetToSaved const handleSave = useCallback(async () => { + if (isListSaving) return + const prevInitialVars = [...initialVarsRef.current] const prevInitialWorkspaceVars = { ...initialWorkspaceVarsRef.current } @@ -964,6 +969,7 @@ export function CredentialsManager() { logger.error('Failed to save environment variables:', error) } }, [ + isListSaving, envVars, workspaceVars, newWorkspaceRows, @@ -1316,9 +1322,9 @@ export function CredentialsManager() { )} @@ -1416,11 +1422,13 @@ export function CredentialsManager() { {hasConflicts && Resolve all conflicts before saving} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx index 46ea1679ae1..d77e761f594 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx @@ -246,12 +246,11 @@ export function IntegrationsManager() { }, [selectedCredential, selectedDisplayNameDraft]) const isDetailsDirty = isDescriptionDirty || isDisplayNameDirty - const [isSavingDetails, setIsSavingDetails] = useState(false) const handleSaveDetails = async () => { - if (!selectedCredential || !isSelectedAdmin || !isDetailsDirty) return + if (!selectedCredential || !isSelectedAdmin || !isDetailsDirty || updateCredential.isPending) + return setDetailsError(null) - setIsSavingDetails(true) try { if (isDisplayNameDirty || isDescriptionDirty) { @@ -263,26 +262,22 @@ export function IntegrationsManager() { if (isDisplayNameDirty) setSelectedDisplayNameDraft((v) => v.trim()) if (isDescriptionDirty) setSelectedDescriptionDraft((v) => v.trim()) } - - await refetchCredentials() } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Failed to save changes' setDetailsError(message) logger.error('Failed to save credential details', error) - } finally { - setIsSavingDetails(false) } } const handleBackAttempt = useCallback(() => { - if (isDetailsDirty && !isSavingDetails) { + if (isDetailsDirty && !updateCredential.isPending) { setShowUnsavedChangesAlert(true) } else { setSelectedCredentialId(null) setSelectedDescriptionDraft('') setSelectedDisplayNameDraft('') } - }, [isDetailsDirty, isSavingDetails]) + }, [isDetailsDirty, updateCredential.isPending]) const handleDiscardChanges = useCallback(() => { setShowUnsavedChangesAlert(false) @@ -1430,9 +1425,9 @@ export function IntegrationsManager() { )} diff --git a/apps/sim/hooks/queries/credentials.ts b/apps/sim/hooks/queries/credentials.ts index 5859970ea08..9267759209f 100644 --- a/apps/sim/hooks/queries/credentials.ts +++ b/apps/sim/hooks/queries/credentials.ts @@ -223,6 +223,45 @@ export function useUpdateWorkspaceCredential() { } return response.json() }, + onMutate: async (variables) => { + await queryClient.cancelQueries({ + queryKey: workspaceCredentialKeys.detail(variables.credentialId), + }) + await queryClient.cancelQueries({ queryKey: workspaceCredentialKeys.lists() }) + + const previousLists = queryClient.getQueriesData({ + queryKey: workspaceCredentialKeys.lists(), + }) + + queryClient.setQueriesData( + { queryKey: workspaceCredentialKeys.lists() }, + (old) => { + if (!old) return old + return old.map((cred) => + cred.id === variables.credentialId + ? { + ...cred, + ...(variables.displayName !== undefined + ? { displayName: variables.displayName } + : {}), + ...(variables.description !== undefined + ? { description: variables.description ?? null } + : {}), + } + : cred + ) + } + ) + + return { previousLists } + }, + onError: (_err, _variables, context) => { + if (context?.previousLists) { + for (const [queryKey, data] of context.previousLists) { + queryClient.setQueryData(queryKey, data) + } + } + }, onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.detail(variables.credentialId),