From ff9d9e95b83cbe3eafe8dae2b9931373ddac2eee Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Thu, 2 Apr 2026 19:11:11 +0000 Subject: [PATCH] fix: add loading state and error handling to API key creation page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/21336?type=bug The "New API Key" page lacks loading feedback during save and silently swallows mutation errors, causing users to rage-click the Save button with no visual response. Fix: Added loading state and error handling to the API key creation page (`SettingsDevelopersApiKeysNew`), matching the exact pattern used in the sibling `SettingsDevelopersApiKeyDetail` component: 1. **Loading state**: Added `useState(false)` for `isLoading`, set to `true` before the async save operation begins, and reset to `false` in a `finally` block. Passed `isLoading` to `SaveAndCancelButtons` which disables the Save button and shows a spinner during the operation. 2. **Error handling**: Wrapped the two sequential mutations (`createApiKey` + `generateApiKeyToken`) in a `try/catch/finally` block. On failure, an error snackbar is displayed via `enqueueErrorSnackBar` — the same hook and pattern used throughout the settings pages. 3. **Silent failure fix**: The `if (!newApiKey) return` silent failure now shows an error snackbar before returning, so users see feedback when the mutation returns no data. These changes prevent rage clicks by: - Showing a loading spinner on the Save button during the async operation - Disabling the button while the operation is in progress (preventing duplicate submissions) - Displaying an error toast if the operation fails (instead of silently doing nothing) --- .../api-keys/SettingsDevelopersApiKeysNew.tsx | 90 +++++++++++-------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx index dc5ad46db6a..661ce4a9231 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx +++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx @@ -7,6 +7,7 @@ import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLo import { SettingsDevelopersRoleSelector } from '@/settings/developers/components/SettingsDevelopersRoleSelector'; import { EXPIRATION_DATES } from '@/settings/developers/constants/ExpirationDates'; import { apiKeyTokenFamilyState } from '@/settings/developers/states/apiKeyTokenFamilyState'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; @@ -27,6 +28,8 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings'; export const SettingsDevelopersApiKeysNew = () => { const { t } = useLingui(); + const { enqueueErrorSnackBar } = useSnackBar(); + const [isLoading, setIsLoading] = useState(false); const [generateOneApiKeyToken] = useMutation(GenerateApiKeyTokenDocument); const navigateSettings = useNavigateSettings(); const { data: rolesData, loading: rolesLoading } = useQuery(GetRolesDocument); @@ -70,50 +73,58 @@ export const SettingsDevelopersApiKeysNew = () => { ); const handleSave = async () => { - if (!formValues.name) return; + if (!formValues.name || !formValues.roleId) return; - const expiresAt = addDays( - new Date(), - formValues.expirationDate ?? 30, - ).toISOString(); + setIsLoading(true); - const roleIdToUse = formValues.roleId; + try { + const expiresAt = addDays( + new Date(), + formValues.expirationDate ?? 30, + ).toISOString(); - if (!roleIdToUse) { - return; - } - - const { data: newApiKeyData } = await createApiKey({ - variables: { - input: { - name: formValues.name.trim(), - expiresAt, - roleId: roleIdToUse, + const { data: newApiKeyData } = await createApiKey({ + variables: { + input: { + name: formValues.name.trim(), + expiresAt, + roleId: formValues.roleId, + }, }, - }, - }); - - const newApiKey = newApiKeyData?.createApiKey; - - if (!newApiKey) { - return; - } - - const tokenData = await generateOneApiKeyToken({ - variables: { - apiKeyId: newApiKey.id, - expiresAt: expiresAt, - }, - }); - - if (isDefined(tokenData.data?.generateApiKeyToken)) { - setApiKeyTokenCallback( - newApiKey.id, - tokenData.data.generateApiKeyToken.token, - ); - navigateSettings(SettingsPath.ApiKeyDetail, { - apiKeyId: newApiKey.id, }); + + const newApiKey = newApiKeyData?.createApiKey; + + if (!newApiKey) { + enqueueErrorSnackBar({ + message: t`Error creating API key`, + }); + + return; + } + + const tokenData = await generateOneApiKeyToken({ + variables: { + apiKeyId: newApiKey.id, + expiresAt: expiresAt, + }, + }); + + if (isDefined(tokenData.data?.generateApiKeyToken)) { + setApiKeyTokenCallback( + newApiKey.id, + tokenData.data.generateApiKeyToken.token, + ); + navigateSettings(SettingsPath.ApiKeyDetail, { + apiKeyId: newApiKey.id, + }); + } + } catch { + enqueueErrorSnackBar({ + message: t`Error creating API key`, + }); + } finally { + setIsLoading(false); } }; @@ -140,6 +151,7 @@ export const SettingsDevelopersApiKeysNew = () => { actionButton={ { navigateSettings(SettingsPath.ApiWebhooks); }}