From 9eec6268d058a6878a431bce8bedeceb99291686 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sat, 1 Aug 2026 10:44:17 +0200 Subject: [PATCH] fix: lock directory during assignment changes --- .../src/app/features/users/users.html | 17 ++++- .../src/app/features/users/users.spec.ts | 74 +++++++++++++++++++ .../src/app/features/users/users.ts | 5 +- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/myteamwallet_frontend_modern/src/app/features/users/users.html b/myteamwallet_frontend_modern/src/app/features/users/users.html index c04da1c..ef1e9a2 100644 --- a/myteamwallet_frontend_modern/src/app/features/users/users.html +++ b/myteamwallet_frontend_modern/src/app/features/users/users.html @@ -15,10 +15,11 @@ type="search" name="directorySearch" [value]="searchDraft()" + [disabled]="assignmentBusyUserId() !== null" (input)="searchDraft.set($any($event.target).value)" /> - + @if (mutationError()) { @@ -138,11 +139,21 @@ diff --git a/myteamwallet_frontend_modern/src/app/features/users/users.spec.ts b/myteamwallet_frontend_modern/src/app/features/users/users.spec.ts index fa7e367..867217d 100644 --- a/myteamwallet_frontend_modern/src/app/features/users/users.spec.ts +++ b/myteamwallet_frontend_modern/src/app/features/users/users.spec.ts @@ -474,6 +474,80 @@ describe('Users directory', () => { http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage()); }); + it('blocks directory search and paging while an assignment mutation is pending, then restores them', () => { + isAdmin.set(true); + create(); + http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([ada], 1, 60, true)); + fixture.detectChanges(); + button('Weiter').click(); + http.expectOne(`${api}?page=2&limit=20`).flush(directoryPage([ada], 2, 60, true)); + fixture.detectChanges(); + button('Zuordnungen verwalten').click(); + fixture.detectChanges(); + http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush( + playersPage({ + data: [ + { + id: 101, + firstName: 'Ada', + lastName: 'Lovelace', + active: true, + team: { id: 5, name: 'First Team', alias: 'first' }, + currentUser: { id: 7, firstName: 'Ada', lastName: 'Lovelace', status: { id: 1, name: 'Active' } }, + }, + ], + }), + ); + fixture.detectChanges(); + childButton( + (fixture.nativeElement as HTMLElement).querySelector('app-player-assignments')!, + 'Verknüpfung lösen', + ).click(); + closeDialog.next(true); + fixture.detectChanges(); + const unlink = http.expectOne(`${adminApi}/7/players/101`); + + const directoryForm = (fixture.nativeElement as HTMLElement).querySelector('.directory-search')!; + const directorySearch = directoryForm.querySelector('input[type="search"]')!; + const directorySearchButton = directoryForm.querySelector('button[type="submit"]')!; + const paging = (fixture.nativeElement as HTMLElement).querySelector('nav.pagination')!; + const [previous, next] = [...paging.querySelectorAll('button')]; + const controlsWereBlocked = directorySearch.disabled && directorySearchButton.disabled && previous.disabled && next.disabled; + + directorySearch.value = 'Other'; + directorySearch.dispatchEvent(new Event('input')); + directoryForm.dispatchEvent(new Event('submit')); + previous.click(); + next.click(); + fixture.detectChanges(); + const unexpectedQueries = http.match((request) => request.url.startsWith(api)); + const mutationStayedActive = !unlink.cancelled; + + if (controlsWereBlocked && unexpectedQueries.length === 0 && mutationStayedActive) { + unlink.flush(ada); + http.expectOne(`${api}?page=2&limit=20`).flush(directoryPage([ada], 2, 60, true)); + http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage()); + fixture.detectChanges(); + expect(directorySearch.disabled).toBe(false); + expect(directorySearchButton.disabled).toBe(false); + expect(previous.disabled).toBe(false); + expect(next.disabled).toBe(false); + directoryForm.dispatchEvent(new Event('submit')); + http.expectOne(`${api}?page=1&limit=20&search=Other`).flush(directoryPage([])); + } else { + for (const query of unexpectedQueries) query.flush(directoryPage([ada], 1, 60, true)); + fixture.detectChanges(); + if (!unlink.cancelled) unlink.flush(ada); + for (const playerLoad of http.match((request) => request.url.startsWith(`${adminApi}/players`))) { + playerLoad.flush(playersPage()); + } + } + + expect(controlsWereBlocked).toBe(true); + expect(unexpectedQueries).toHaveLength(0); + expect(mutationStayedActive).toBe(true); + }); + it('shows a persistent explanation for disabled self-deactivation', () => { isAdmin.set(true); currentUser.set({ id: 1, firstName: 'Grace', lastName: 'Admin', role: { id: 1 } }); diff --git a/myteamwallet_frontend_modern/src/app/features/users/users.ts b/myteamwallet_frontend_modern/src/app/features/users/users.ts index 47e8278..ab30355 100644 --- a/myteamwallet_frontend_modern/src/app/features/users/users.ts +++ b/myteamwallet_frontend_modern/src/app/features/users/users.ts @@ -90,6 +90,7 @@ export class Users { } protected submitSearch(): void { + if (this.assignmentBusyUserId() !== null) return; const search = this.searchDraft().trim(); if (this.loading() && search === this.search() && this.page() === 1) return; this.search.set(search); @@ -98,13 +99,13 @@ export class Users { } protected previousPage(): void { - if (this.page() <= 1 || this.loading()) return; + if (this.page() <= 1 || this.loading() || this.assignmentBusyUserId() !== null) return; this.page.update((value) => value - 1); this.loadDirectory(); } protected nextPage(): void { - if (!this.directory()?.hasNextPage || this.loading()) return; + if (!this.directory()?.hasNextPage || this.loading() || this.assignmentBusyUserId() !== null) return; this.page.update((value) => value + 1); this.loadDirectory(); }