fix: add loading state and error handling to API key creation page

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)
This commit is contained in:
Sonarly Claude Code
2026-04-02 19:11:11 +00:00
parent 04d22feef6
commit ff9d9e95b8
@@ -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={
<SaveAndCancelButtons
isSaveDisabled={!canSave}
isLoading={isLoading}
onCancel={() => {
navigateSettings(SettingsPath.ApiWebhooks);
}}