diff --git a/app/Community/Controllers/UserSettingsController.php b/app/Community/Controllers/UserSettingsController.php
index afd0bf827f..b99f875034 100644
--- a/app/Community/Controllers/UserSettingsController.php
+++ b/app/Community/Controllers/UserSettingsController.php
@@ -97,10 +97,16 @@ public function storeUsernameChangeRequest(StoreUsernameChangeRequest $request):
/** @var User $user */
$user = $request->user();
- UserUsername::create([
- 'user_id' => $user->id,
- 'username' => $data->newDisplayName,
- ]);
+ $isOnlyCapitalizationChange = strtolower($user->display_name) === strtolower($data->newDisplayName);
+ if ($isOnlyCapitalizationChange) {
+ $user->display_name = $data->newDisplayName;
+ $user->save();
+ } else {
+ UserUsername::create([
+ 'user_id' => $user->id,
+ 'username' => $data->newDisplayName,
+ ]);
+ }
return response()->json(['success' => true]);
}
diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php
index 363e9085a7..302d792c1e 100755
--- a/app/Community/RouteServiceProvider.php
+++ b/app/Community/RouteServiceProvider.php
@@ -374,7 +374,7 @@ protected function mapWebRoutes(): void
Route::put('email', [UserSettingsController::class, 'updateEmail'])->name('api.settings.email.update');
Route::post('username-change-request', [UserSettingsController::class, 'storeUsernameChangeRequest'])
- ->name('api.settings.username-change-request.store');
+ ->name('api.settings.name-change-request.store');
Route::delete('keys/web', [UserSettingsController::class, 'resetWebApiKey'])->name('api.settings.keys.web.destroy');
Route::delete('keys/connect', [UserSettingsController::class, 'resetConnectApiKey'])->name('api.settings.keys.connect.destroy');
diff --git a/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx b/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx
index 07bbe1b474..3d6d6187cb 100644
--- a/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx
+++ b/resources/js/features/settings/components/ChangeUsernameSectionCard/ChangeUsernameSectionCard.test.tsx
@@ -8,6 +8,12 @@ import { requestedUsernameAtom } from '../../state/settings.atoms';
import { ChangeUsernameSectionCard } from './ChangeUsernameSectionCard';
describe('Component: ChangeUsernameSectionCard', () => {
+ const originalLocation = window.location;
+
+ afterEach(() => {
+ window.location = originalLocation;
+ });
+
it('renders without crashing', () => {
// ARRANGE
const { container } = render(, {
@@ -154,7 +160,7 @@ describe('Component: ChangeUsernameSectionCard', () => {
await userEvent.click(screen.getByRole('button', { name: /update/i }));
// ASSERT
- expect(postSpy).toHaveBeenCalledWith(route('api.settings.username-change-request.store'), {
+ expect(postSpy).toHaveBeenCalledWith(route('api.settings.name-change-request.store'), {
newDisplayName: 'NewName',
});
});
@@ -216,4 +222,73 @@ describe('Component: ChangeUsernameSectionCard', () => {
expect(screen.getByText(/something went wrong/i)).toBeVisible();
});
});
+
+ it('given the user submits a username that only differs in case, auto-approves without confirmation', async () => {
+ // ARRANGE
+ delete (window as any).location;
+ window.location = {
+ ...originalLocation,
+ reload: vi.fn(),
+ };
+
+ const confirmSpy = vi.spyOn(window, 'confirm');
+ const reloadSpy = vi.spyOn(window.location, 'reload').mockImplementation(() => {});
+ const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({
+ data: {
+ success: true,
+ },
+ });
+
+ render(, {
+ pageProps: {
+ auth: { user: createAuthenticatedUser({ displayName: 'testuser' }) },
+ can: { createUsernameChangeRequest: true },
+ },
+ });
+
+ // ACT
+ await userEvent.type(screen.getAllByLabelText(/new username/i)[0], 'TestUser');
+ await userEvent.type(screen.getByLabelText(/confirm new username/i), 'TestUser');
+ await userEvent.click(screen.getByRole('button', { name: /update/i }));
+
+ // ASSERT
+ expect(confirmSpy).not.toHaveBeenCalled();
+ expect(postSpy).toHaveBeenCalledWith(route('api.settings.name-change-request.store'), {
+ newDisplayName: 'TestUser',
+ });
+ expect(reloadSpy).toHaveBeenCalled();
+ });
+
+ it('given the API returns success for a case change, reloads the page', async () => {
+ // ARRANGE
+ delete (window as any).location;
+ window.location = {
+ ...originalLocation,
+ reload: vi.fn(),
+ };
+
+ const reloadSpy = vi.spyOn(window.location, 'reload').mockImplementation(() => {});
+ vi.spyOn(axios, 'post').mockResolvedValueOnce({
+ data: {
+ success: true,
+ },
+ });
+
+ render(, {
+ pageProps: {
+ auth: { user: createAuthenticatedUser({ displayName: 'testuser' }) },
+ can: { createUsernameChangeRequest: true },
+ },
+ });
+
+ // ACT
+ await userEvent.type(screen.getAllByLabelText(/new username/i)[0], 'TestUser');
+ await userEvent.type(screen.getByLabelText(/confirm new username/i), 'TestUser');
+ await userEvent.click(screen.getByRole('button', { name: /update/i }));
+
+ // ASSERT
+ await waitFor(() => {
+ expect(reloadSpy).toHaveBeenCalled();
+ });
+ });
});
diff --git a/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts b/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts
index 394f607336..b7633a7413 100644
--- a/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts
+++ b/resources/js/features/settings/components/ChangeUsernameSectionCard/useChangeUsernameForm.ts
@@ -59,11 +59,19 @@ export function useChangeUsernameForm() {
const mutation = useMutation({
mutationFn: (formValues: FormValues) => {
- return axios.post(route('api.settings.username-change-request.store'), {
+ return axios.post(route('api.settings.name-change-request.store'), {
newDisplayName: formValues.newUsername,
});
},
onSuccess: (_, { newUsername }) => {
+ const wasAutoApproved = auth!.user.displayName.toLowerCase() === newUsername.toLowerCase();
+
+ if (wasAutoApproved) {
+ window.location.reload();
+
+ return;
+ }
+
setRequestedUsername(newUsername);
},
});
@@ -73,7 +81,12 @@ export function useChangeUsernameForm() {
'You can only request a new username once every 30 days, even if your new username is not approved. Are you sure you want to do this?',
);
- if (!confirm(confirmationMessage)) {
+ // If the user just wants a case change, no need to confirm.
+ // We'll make the change instantly without needing approval.
+ const mustShowConfirm =
+ auth!.user.displayName.toLowerCase() !== formValues.newUsername.toLowerCase();
+
+ if (mustShowConfirm && !confirm(confirmationMessage)) {
return;
}
diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts
index b09137829a..a94fce601f 100644
--- a/resources/js/types/generated.d.ts
+++ b/resources/js/types/generated.d.ts
@@ -130,8 +130,8 @@ declare namespace App.Community.Data {
export type UserSettingsPageProps = {
userSettings: App.Data.User;
can: App.Data.UserPermissions;
- requestedUsername: string | null;
displayableRoles: Array;
+ requestedUsername: string | null;
};
}
declare namespace App.Community.Enums {
diff --git a/resources/js/ziggy.d.ts b/resources/js/ziggy.d.ts
index 5b26293693..25a9d7e658 100644
--- a/resources/js/ziggy.d.ts
+++ b/resources/js/ziggy.d.ts
@@ -581,7 +581,7 @@ declare module 'ziggy-js' {
"api.settings.preferences.update": [],
"api.settings.password.update": [],
"api.settings.email.update": [],
- "api.settings.username-change-request.store": [],
+ "api.settings.name-change-request.store": [],
"api.settings.keys.web.destroy": [],
"api.settings.keys.connect.destroy": [],
"api.active-player.index": [],
diff --git a/tests/Feature/Community/Controllers/UserSettingsControllerTest.php b/tests/Feature/Community/Controllers/UserSettingsControllerTest.php
index 2cd02fcbd9..3240a11111 100644
--- a/tests/Feature/Community/Controllers/UserSettingsControllerTest.php
+++ b/tests/Feature/Community/Controllers/UserSettingsControllerTest.php
@@ -125,7 +125,7 @@ public function testUpdateUsername(): void
// Act
$response = $this->actingAs($user)
- ->postJson(route('api.settings.username-change-request.store'), [
+ ->postJson(route('api.settings.name-change-request.store'), [
'newDisplayName' => 'Scott123456712',
]);
@@ -139,6 +139,35 @@ public function testUpdateUsername(): void
]);
}
+ public function testUpdateUsernameWithOnlyCapitalizationChange(): void
+ {
+ // Arrange
+ $this->withoutMiddleware();
+
+ /** @var User $user */
+ $user = User::factory()->create([
+ 'User' => 'scott',
+ 'display_name' => 'scott',
+ ]);
+
+ // Act
+ $response = $this->actingAs($user)
+ ->postJson(route('api.settings.name-change-request.store'), [
+ 'newDisplayName' => 'Scott',
+ ]);
+
+ // Assert
+ $response->assertStatus(200);
+
+ $user = $user->fresh();
+ $this->assertEquals('Scott', $user->display_name); // instantly makes the update
+
+ $this->assertDatabaseMissing('user_usernames', [ // does not make an approval request record
+ 'user_id' => $user->id,
+ 'username' => 'Scott',
+ ]);
+ }
+
public function testUpdateProfile(): void
{
// Arrange