mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
add pending email cancel button
This commit is contained in:
committed by
Constantin Graf
parent
734aab6353
commit
24c19e7e7c
@@ -81,6 +81,7 @@ test('profile photo can be uploaded, persists across reload, and can be removed'
|
||||
reloadedForm.getByRole('button', { name: 'Remove Photo' }).click(),
|
||||
]);
|
||||
await expect(reloadedProfilePhoto).toHaveAttribute('src', /ui-avatars\.com/);
|
||||
await expect(reloadedForm.getByRole('button', { name: 'Remove Photo' })).toBeHidden();
|
||||
|
||||
await page.reload();
|
||||
const finalForm = profileInformationForm(page);
|
||||
@@ -88,6 +89,7 @@ test('profile photo can be uploaded, persists across reload, and can be removed'
|
||||
'src',
|
||||
/ui-avatars\.com/
|
||||
);
|
||||
await expect(finalForm.getByRole('button', { name: 'Remove Photo' })).toBeHidden();
|
||||
});
|
||||
|
||||
test('field-level validation errors render inline when the server returns 422', async ({
|
||||
@@ -206,6 +208,41 @@ test('clicking resend sends a second verification email and shows confirmation',
|
||||
expect(afterCount).toBeGreaterThan(beforeCount);
|
||||
});
|
||||
|
||||
test('cancelling a pending email change clears it and hides the banner', async ({ page, ctx }) => {
|
||||
const { email: currentEmail } = await getCurrentUserViaApi(ctx);
|
||||
const newEmail = `cancel+${Date.now()}@test.com`;
|
||||
|
||||
await goToProfilePage(page);
|
||||
await page.getByLabel('Email').fill(newEmail);
|
||||
await saveProfileForm(page);
|
||||
|
||||
// The pending-email banner is shown with the cancel control.
|
||||
await expect(page.getByText('A verification link was sent to')).toBeVisible();
|
||||
await expect(page.getByText(newEmail)).toBeVisible();
|
||||
const cancelButton = page.getByRole('button', { name: 'Cancel email change' });
|
||||
await expect(cancelButton).toBeVisible();
|
||||
|
||||
// Cancelling clears the pending email server-side (204).
|
||||
await Promise.all([
|
||||
page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/reset-pending-email') &&
|
||||
response.request().method() === 'POST' &&
|
||||
response.status() === 204
|
||||
),
|
||||
cancelButton.click(),
|
||||
]);
|
||||
|
||||
// The banner disappears and the email field still shows the current address.
|
||||
await expect(page.getByText('A verification link was sent to')).toBeHidden();
|
||||
await expect(page.getByLabel('Email')).toHaveValue(currentEmail);
|
||||
|
||||
// The cancellation is persistent — still gone after a reload.
|
||||
await page.reload();
|
||||
await expect(page.getByText('A verification link was sent to')).toBeHidden();
|
||||
await expect(page.getByLabel('Email')).toHaveValue(currentEmail);
|
||||
});
|
||||
|
||||
test('re-submitting the same pending email does not send another verification email', async ({
|
||||
page,
|
||||
request,
|
||||
|
||||
@@ -5,11 +5,13 @@ import axios from 'axios';
|
||||
import ActionMessage from '@/Components/ActionMessage.vue';
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import { Field, FieldError, FieldLabel } from '@/packages/ui/src/field';
|
||||
import { Button } from '@/packages/ui/src/Buttons';
|
||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
||||
import {
|
||||
useResendUserEmailVerificationMutation,
|
||||
useResetUserPendingEmailMutation,
|
||||
useUpdateUserMutation,
|
||||
useUserQuery,
|
||||
} from '@/utils/useUserQuery';
|
||||
@@ -18,6 +20,7 @@ import type { UpdateUserBody, User } from '@/packages/api/src';
|
||||
const { user } = useUserQuery();
|
||||
const updateUser = useUpdateUserMutation();
|
||||
const resendVerification = useResendUserEmailVerificationMutation();
|
||||
const resetPendingEmail = useResetUserPendingEmailMutation();
|
||||
|
||||
const name = ref('');
|
||||
const email = ref('');
|
||||
@@ -152,6 +155,17 @@ async function clickResend() {
|
||||
}
|
||||
}
|
||||
|
||||
async function clickCancelEmailChange() {
|
||||
if (!user.value || resetPendingEmail.isPending.value) return;
|
||||
try {
|
||||
// Clears pending_email on the server; the pending banner hides once the
|
||||
// me query refetches. The email field already shows the current address.
|
||||
await resetPendingEmail.mutateAsync(user.value.id);
|
||||
} catch {
|
||||
// notification handled by mutation
|
||||
}
|
||||
}
|
||||
|
||||
function flashSaved() {
|
||||
recentlySaved.value = true;
|
||||
setTimeout(() => (recentlySaved.value = false), 2000);
|
||||
@@ -259,15 +273,26 @@ const page = usePage<{
|
||||
<span class="font-medium">{{ pendingEmail }}</span
|
||||
>. Click the link in the email to confirm the change.
|
||||
</p>
|
||||
<button
|
||||
v-if="!resendCooldown"
|
||||
type="button"
|
||||
class="mt-1 underline text-text-secondary hover:text-text-primary rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
:disabled="!isUserLoaded || resendVerification.isPending.value"
|
||||
@click="clickResend">
|
||||
Resend verification email
|
||||
</button>
|
||||
<p v-else class="mt-1 font-medium text-green-400">Verification email sent.</p>
|
||||
<div class="mt-2 -ms-3 flex flex-wrap items-center gap-x-1 gap-y-1">
|
||||
<Button
|
||||
v-if="!resendCooldown"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
:disabled="!isUserLoaded || resendVerification.isPending.value"
|
||||
@click="clickResend">
|
||||
Resend verification email
|
||||
</Button>
|
||||
<p v-else class="ms-3 font-medium text-green-400">Verification email sent.</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
:disabled="!isUserLoaded || resetPendingEmail.isPending.value"
|
||||
@click="clickCancelEmailChange">
|
||||
Cancel email change
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -4573,6 +4573,38 @@ The report is considered public if the `is_public` field is set to &#x
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/v1/users/:user/reset-pending-email',
|
||||
alias: 'resetUserPendingEmail',
|
||||
description: `This endpoint is independent of the organization.`,
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'user',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
description: `Unauthenticated`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
description: `Authorization error`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
description: `Not found`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/v1/users/:user/resend-email-verification',
|
||||
|
||||
@@ -93,3 +93,21 @@ export function useResendUserEmailVerificationMutation() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useResetUserPendingEmailMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (userId: string) => {
|
||||
return handleApiRequestNotifications(
|
||||
() => api.resetUserPendingEmail(undefined, { params: { user: userId } }),
|
||||
'Email change canceled',
|
||||
'Failed to cancel email change'
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ME_QUERY_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user