Skip to content

Commit 86a6036

Browse files
markgohoclaude
andauthored
fix(members): persist admin state across navigation (#82)
* fix(members): persist admin state across navigation Keep admin dashboard and list data alive across route changes by moving shared resources into root-provided state services and invalidating them after relevant mutations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(members): gate admin detail loads and refresh shared state Prevent admin message and match request detail views from loading before route params exist, and invalidate the unclaimed list after email updates. Add regression tests for idle-before-init and shared-state invalidation to keep the persistence fix covered. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(members): remove service-level admin detail tests Drop the admin detail service specs so coverage stays at user-facing boundaries, in line with the project's testing philosophy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a5494c commit 86a6036

16 files changed

Lines changed: 212 additions & 91 deletions

members/src/app/admin/admin-dashboard.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { ChangeDetectionStrategy, Component, computed, inject, resource } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
22
import { RouterLink } from '@angular/router';
3-
import { AdminMembersService } from './services/admin-members.service';
4-
import { AdminMatchRequestsService } from './services/admin-match-requests.service';
5-
import { AdminMessagesService } from './services/admin-messages.service';
3+
import { AdminMatchRequestsStateService } from './state/admin-match-requests-state.service';
4+
import { AdminMembersStateService } from './state/admin-members-state.service';
5+
import { AdminMessagesStateService } from './state/admin-messages-state.service';
6+
import { AdminUnclaimedStateService } from './state/admin-unclaimed-state.service';
67

78
@Component({
89
imports: [RouterLink],
@@ -11,26 +12,22 @@ import { AdminMessagesService } from './services/admin-messages.service';
1112
changeDetection: ChangeDetectionStrategy.OnPush,
1213
})
1314
export class AdminDashboard {
14-
private adminMembersService = inject(AdminMembersService);
15-
private adminMatchRequestsService = inject(AdminMatchRequestsService);
16-
private adminMessagesService = inject(AdminMessagesService);
17-
18-
// Load stats for the dashboard cards
19-
protected membersResource = resource({
20-
loader: () => this.adminMembersService.listMembers(),
21-
});
22-
23-
protected unclaimedResource = resource({
24-
loader: () => this.adminMembersService.listUnclaimedProfiles(),
25-
});
26-
27-
protected matchRequestsResource = resource({
28-
loader: () => this.adminMatchRequestsService.listMatchRequests(),
29-
});
30-
31-
protected messagesResource = resource({
32-
loader: () => this.adminMessagesService.listMessages(),
33-
});
15+
private membersState = inject(AdminMembersStateService);
16+
private unclaimedState = inject(AdminUnclaimedStateService);
17+
private matchRequestsState = inject(AdminMatchRequestsStateService);
18+
private messagesState = inject(AdminMessagesStateService);
19+
20+
protected membersResource = this.membersState.membersResource;
21+
protected unclaimedResource = this.unclaimedState.unclaimedResource;
22+
protected matchRequestsResource = this.matchRequestsState.matchRequestsResource;
23+
protected messagesResource = this.messagesState.messagesResource;
24+
25+
constructor() {
26+
this.membersState.initialize();
27+
this.unclaimedState.initialize();
28+
this.matchRequestsState.initialize();
29+
this.messagesState.initialize();
30+
}
3431

3532
protected totalMembers = computed(() => {
3633
return this.membersResource.hasValue() ? (this.membersResource.value()?.total ?? 0) : 0;

members/src/app/admin/match-requests/admin-match-request-detail/admin-match-request-detail.service.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
import { Injectable, computed, inject, resource, signal } from '@angular/core';
1+
import { Injectable, computed, inject, resource, signal, type Signal } from '@angular/core';
22
import { AdminMatchRequestsService } from '../../services/admin-match-requests.service';
3+
import { AdminMatchRequestsStateService } from '../../state/admin-match-requests-state.service';
34

45
@Injectable()
56
export class AdminMatchRequestDetailService {
67
private adminMatchRequestsService = inject(AdminMatchRequestsService);
8+
private matchRequestsState = inject(AdminMatchRequestsStateService);
79

8-
// Signal for the current match request id (set from component via effect)
9-
readonly idSignal = signal<string>('');
10+
// Signal for the current match request id (set from component input)
11+
private idSignal = signal<Signal<string> | undefined>(undefined);
1012

1113
// Resource automatically loads match request based on id
1214
readonly matchRequestResource = resource({
13-
params: () => ({ id: this.idSignal() }),
15+
params: () => {
16+
const idSignal = this.idSignal();
17+
return idSignal ? { id: idSignal() } : undefined;
18+
},
1419
loader: ({ params }) => this.adminMatchRequestsService.getMatchRequest(params.id),
1520
});
1621

@@ -25,6 +30,13 @@ export class AdminMatchRequestDetailService {
2530
readonly successMessage = signal<string | undefined>(undefined);
2631
readonly actionError = signal<string | undefined>(undefined);
2732

33+
/**
34+
* Initialize the service with the match request id signal from component input
35+
*/
36+
init(idSignal: Signal<string>): void {
37+
this.idSignal.set(idSignal);
38+
}
39+
2840
/**
2941
* Update the status (sent field) of the match request
3042
*/
@@ -36,7 +48,8 @@ export class AdminMatchRequestDetailService {
3648
try {
3749
await this.adminMatchRequestsService.updateMatchRequestStatus(id, sent);
3850
this.successMessage.set(`Match request marked as ${sent ? 'processed' : 'pending'}`);
39-
this.matchRequestResource.reload(); // Reload to get updated data
51+
this.matchRequestResource.reload();
52+
this.matchRequestsState.invalidate();
4053
} catch (error) {
4154
console.error('Error updating match request status:', error);
4255
this.actionError.set('Failed to update match request status.');

members/src/app/admin/match-requests/admin-match-request-detail/admin-match-request-detail.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
ChangeDetectionStrategy,
44
Component,
55
computed,
6-
effect,
76
inject,
87
input,
98
signal,
@@ -55,10 +54,7 @@ export class AdminMatchRequestDetail {
5554
});
5655

5756
constructor() {
58-
// Sync route id parameter to service signal
59-
effect(() => {
60-
this.service.idSignal.set(this.id());
61-
});
57+
this.service.init(this.id);
6258
}
6359

6460
protected parseDueDate(dueDate: DueDate): Date | undefined {

members/src/app/admin/match-requests/admin-match-requests.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ChangeDetectionStrategy, Component, computed, inject, resource } from '@angular/core';
2-
import { AdminMatchRequestsService } from '../services/admin-match-requests.service';
1+
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2+
import { AdminMatchRequestsStateService } from '../state/admin-match-requests-state.service';
33
import { MatchRequestsTable } from './match-requests-table/match-requests-table';
44

55
@Component({
@@ -9,11 +9,13 @@ import { MatchRequestsTable } from './match-requests-table/match-requests-table'
99
changeDetection: ChangeDetectionStrategy.OnPush,
1010
})
1111
export class AdminMatchRequests {
12-
private adminMatchRequestsService = inject(AdminMatchRequestsService);
12+
private matchRequestsState = inject(AdminMatchRequestsStateService);
1313

14-
protected matchRequestsResource = resource({
15-
loader: () => this.adminMatchRequestsService.listMatchRequests(100, 0, 'all'),
16-
});
14+
protected matchRequestsResource = this.matchRequestsState.matchRequestsResource;
15+
16+
constructor() {
17+
this.matchRequestsState.initialize();
18+
}
1719

1820
protected totalRequests = computed(() => {
1921
return this.matchRequestsResource.hasValue()

members/src/app/admin/members/admin-members.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ChangeDetectionStrategy, Component, computed, inject, resource } from '@angular/core';
2-
import { AdminMembersService } from '../services/admin-members.service';
1+
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2+
import { AdminMembersStateService } from '../state/admin-members-state.service';
33
import { ActiveMembersTable } from '../users/active-members-table/active-members-table';
44

55
@Component({
@@ -9,11 +9,13 @@ import { ActiveMembersTable } from '../users/active-members-table/active-members
99
changeDetection: ChangeDetectionStrategy.OnPush,
1010
})
1111
export class AdminMembers {
12-
private adminMembersService = inject(AdminMembersService);
12+
private membersState = inject(AdminMembersStateService);
1313

14-
protected membersResource = resource({
15-
loader: () => this.adminMembersService.listMembers(),
16-
});
14+
protected membersResource = this.membersState.membersResource;
15+
16+
constructor() {
17+
this.membersState.initialize();
18+
}
1719

1820
protected totalMembers = computed(() => {
1921
return this.membersResource.hasValue() ? (this.membersResource.value()?.total ?? 0) : 0;

members/src/app/admin/messages/admin-message-detail/admin-message-detail.service.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
import { Injectable, computed, inject, resource, signal } from '@angular/core';
1+
import { Injectable, computed, inject, resource, signal, type Signal } from '@angular/core';
22
import { AdminMessagesService } from '../../services/admin-messages.service';
3+
import { AdminMessagesStateService } from '../../state/admin-messages-state.service';
34

45
@Injectable()
56
export class AdminMessageDetailService {
67
private adminMessagesService = inject(AdminMessagesService);
8+
private messagesState = inject(AdminMessagesStateService);
79

8-
// Signal for the current message id (set from component via effect)
9-
readonly idSignal = signal<string>('');
10+
// Signal for the current message id (set from component input)
11+
private idSignal = signal<Signal<string> | undefined>(undefined);
1012

1113
// Resource automatically loads message based on id
1214
readonly messageResource = resource({
13-
params: () => ({ id: this.idSignal() }),
15+
params: () => {
16+
const idSignal = this.idSignal();
17+
return idSignal ? { id: idSignal() } : undefined;
18+
},
1419
loader: ({ params }) => this.adminMessagesService.getMessage(params.id),
1520
});
1621

@@ -25,6 +30,13 @@ export class AdminMessageDetailService {
2530
readonly successMessage = signal<string | undefined>(undefined);
2631
readonly actionError = signal<string | undefined>(undefined);
2732

33+
/**
34+
* Initialize the service with the message id signal from component input
35+
*/
36+
init(idSignal: Signal<string>): void {
37+
this.idSignal.set(idSignal);
38+
}
39+
2840
/**
2941
* Update the status (sent field) of the message
3042
*/
@@ -36,7 +48,8 @@ export class AdminMessageDetailService {
3648
try {
3749
await this.adminMessagesService.updateMessageStatus(id, sent);
3850
this.successMessage.set(`Message marked as ${sent ? 'processed' : 'pending'}`);
39-
this.messageResource.reload(); // Reload to get updated data
51+
this.messageResource.reload();
52+
this.messagesState.invalidate();
4053
} catch (error) {
4154
console.error('Error updating message status:', error);
4255
this.actionError.set('Failed to update message status.');

members/src/app/admin/messages/admin-message-detail/admin-message-detail.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AdminMessageDetailService } from './admin-message-detail.service';
88

99
// Mock the service
1010
class MockAdminMessageDetailService {
11-
idSignal = signal<string>('');
11+
init = vi.fn();
1212
messageResource = {
1313
value: signal<Message | undefined>(undefined),
1414
isLoading: signal(false),

members/src/app/admin/messages/admin-message-detail/admin-message-detail.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import { DatePipe } from '@angular/common';
2-
import {
3-
ChangeDetectionStrategy,
4-
Component,
5-
effect,
6-
inject,
7-
input,
8-
signal,
9-
viewChild,
10-
} from '@angular/core';
2+
import { ChangeDetectionStrategy, Component, inject, input, signal, viewChild } from '@angular/core';
113
import { Tag } from '../../../tag/tag';
124
import { ConfirmDialog } from '../../../shared/confirm-dialog/confirm-dialog';
135
import { AlertBanner } from '../../../shared/alert-banner/alert-banner';
@@ -46,10 +38,7 @@ export class AdminMessageDetail {
4638
});
4739

4840
constructor() {
49-
// Sync route id parameter to service signal
50-
effect(() => {
51-
this.service.idSignal.set(this.id());
52-
});
41+
this.service.init(this.id);
5342
}
5443

5544
protected showMarkProcessedConfirm(): void {

members/src/app/admin/messages/admin-messages.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ChangeDetectionStrategy, Component, computed, inject, resource } from '@angular/core';
2-
import { AdminMessagesService } from '../services/admin-messages.service';
1+
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2+
import { AdminMessagesStateService } from '../state/admin-messages-state.service';
33
import { MessagesTable } from './messages-table/messages-table';
44

55
@Component({
@@ -9,11 +9,13 @@ import { MessagesTable } from './messages-table/messages-table';
99
changeDetection: ChangeDetectionStrategy.OnPush,
1010
})
1111
export class AdminMessages {
12-
private adminMessagesService = inject(AdminMessagesService);
12+
private messagesState = inject(AdminMessagesStateService);
1313

14-
protected messagesResource = resource({
15-
loader: () => this.adminMessagesService.listMessages(100, 0, 'all'),
16-
});
14+
protected messagesResource = this.messagesState.messagesResource;
15+
16+
constructor() {
17+
this.messagesState.initialize();
18+
}
1719

1820
protected totalMessages = computed(() => {
1921
return this.messagesResource.hasValue() ? (this.messagesResource.value()?.total ?? 0) : 0;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Injectable, inject, resource, signal } from '@angular/core';
2+
import { AdminMatchRequestsService } from '../services/admin-match-requests.service';
3+
4+
@Injectable({
5+
providedIn: 'root',
6+
})
7+
export class AdminMatchRequestsStateService {
8+
private adminMatchRequestsService = inject(AdminMatchRequestsService);
9+
private initialized = signal(false);
10+
11+
readonly matchRequestsResource = resource({
12+
params: () => (this.initialized() ? {} : undefined),
13+
loader: () => this.adminMatchRequestsService.listMatchRequests(100, 0, 'all'),
14+
});
15+
16+
initialize(): void {
17+
this.initialized.set(true);
18+
}
19+
20+
invalidate(): void {
21+
this.matchRequestsResource.reload();
22+
}
23+
}

0 commit comments

Comments
 (0)