Skip to content

Commit 391944b

Browse files
authored
Feat(ENG-8625): add Recent Activity to registrations (#315)
* feat(registration-recent-activity): add Recent Activity to registrations * feat(registration-recent-activity): code fixes regarding comments * feat(registration-recent-activity): second round of code fixes regarding review comments * feat(registration-recent-activity): run lint fix
1 parent e8c4d67 commit 391944b

26 files changed

Lines changed: 956 additions & 90 deletions
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { InjectionToken } from '@angular/core';
22

3+
import { AppEnvironment } from '@shared/models/environment.model';
4+
35
import { environment } from 'src/environments/environment';
46

5-
export const ENVIRONMENT = new InjectionToken<typeof environment>('App Environment', {
7+
export const ENVIRONMENT = new InjectionToken<AppEnvironment>('App Environment', {
68
providedIn: 'root',
7-
factory: () => environment,
9+
factory: () => environment as AppEnvironment,
810
});

src/app/core/constants/nav-items.constant.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ export const REGISTRATION_MENU_ITEMS: MenuItem[] = [
171171
visible: true,
172172
routerLinkActiveOptions: { exact: false },
173173
},
174+
{
175+
id: 'registration-recent-activity',
176+
label: 'navigation.recentActivity',
177+
routerLink: 'recent-activity',
178+
visible: true,
179+
routerLinkActiveOptions: { exact: true },
180+
},
174181
];
175182

176183
export const MENU_ITEMS: MenuItem[] = [
Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,100 @@
1+
import { provideStore, Store } from '@ngxs/store';
2+
3+
import { TranslateService } from '@ngx-translate/core';
4+
5+
import { of } from 'rxjs';
6+
7+
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
8+
import { provideHttpClientTesting } from '@angular/common/http/testing';
19
import { ComponentFixture, TestBed } from '@angular/core/testing';
10+
import { ActivatedRoute } from '@angular/router';
11+
12+
import { ActivityLogDisplayService } from '@shared/services';
13+
import { GetActivityLogs } from '@shared/stores/activity-logs';
14+
import { ActivityLogsState } from '@shared/stores/activity-logs/activity-logs.state';
215

316
import { RecentActivityComponent } from './recent-activity.component';
417

518
describe('RecentActivityComponent', () => {
6-
let component: RecentActivityComponent;
719
let fixture: ComponentFixture<RecentActivityComponent>;
20+
let store: Store;
821

922
beforeEach(async () => {
1023
await TestBed.configureTestingModule({
1124
imports: [RecentActivityComponent],
25+
providers: [
26+
provideStore([ActivityLogsState]),
27+
28+
provideHttpClient(withInterceptorsFromDi()),
29+
provideHttpClientTesting(),
30+
31+
{
32+
provide: TranslateService,
33+
useValue: {
34+
instant: (k: string) => k,
35+
get: () => of(''),
36+
stream: () => of(''),
37+
onLangChange: of({}),
38+
onDefaultLangChange: of({}),
39+
onTranslationChange: of({}),
40+
},
41+
},
42+
43+
{ provide: ActivatedRoute, useValue: { snapshot: { params: { id: 'proj123' } }, parent: null } },
44+
{ provide: ActivityLogDisplayService, useValue: { getActivityDisplay: jest.fn().mockReturnValue('FMT') } },
45+
],
1246
}).compileComponents();
1347

48+
store = TestBed.inject(Store);
49+
store.reset({
50+
activityLogs: {
51+
activityLogs: { data: [], isLoading: false, error: null, totalCount: 0 },
52+
},
53+
} as any);
54+
1455
fixture = TestBed.createComponent(RecentActivityComponent);
15-
component = fixture.componentInstance;
56+
fixture.componentRef.setInput('pageSize', 10);
1657
fixture.detectChanges();
1758
});
1859

1960
it('should create', () => {
20-
expect(component).toBeTruthy();
61+
expect(fixture.componentInstance).toBeTruthy();
62+
});
63+
64+
it('formats activity logs using ActivityLogDisplayService', () => {
65+
store.reset({
66+
activityLogs: {
67+
activityLogs: {
68+
data: [{ id: 'log1', date: '2024-01-01T00:00:00Z' }],
69+
isLoading: false,
70+
error: null,
71+
totalCount: 1,
72+
},
73+
},
74+
} as any);
75+
76+
fixture.detectChanges();
77+
78+
const formatted = fixture.componentInstance.formattedActivityLogs();
79+
expect(formatted.length).toBe(1);
80+
expect(formatted[0].formattedActivity).toBe('FMT');
81+
});
82+
83+
it('dispatches GetActivityLogs with numeric page and pageSize on page change', () => {
84+
const dispatchSpy = jest.spyOn(store, 'dispatch');
85+
fixture.componentInstance.onPageChange({ page: 2 } as any);
86+
87+
expect(dispatchSpy).toHaveBeenCalled();
88+
const action = dispatchSpy.mock.calls.at(-1)?.[0] as GetActivityLogs;
89+
90+
expect(action).toBeInstanceOf(GetActivityLogs);
91+
expect(action.projectId).toBe('proj123');
92+
expect(action.page).toBe(3);
93+
expect(action.pageSize).toBe(10);
94+
});
95+
96+
it('computes firstIndex correctly', () => {
97+
fixture.componentInstance['currentPage'].set(3);
98+
expect(fixture.componentInstance['firstIndex']()).toBe(20);
2199
});
22100
});

src/app/features/project/overview/components/recent-activity/recent-activity.component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ export class RecentActivityComponent {
2626

2727
readonly pageSize = input.required<number>();
2828
currentPage = signal<number>(1);
29+
2930
activityLogs = select(ActivityLogsSelectors.getActivityLogs);
3031
totalCount = select(ActivityLogsSelectors.getActivityLogsTotalCount);
3132
isLoading = select(ActivityLogsSelectors.getActivityLogsLoading);
33+
3234
firstIndex = computed(() => (this.currentPage() - 1) * this.pageSize());
3335

34-
actions = createDispatchMap({
35-
getActivityLogs: GetActivityLogs,
36-
});
36+
actions = createDispatchMap({ getActivityLogs: GetActivityLogs });
3737

3838
formattedActivityLogs = computed(() => {
3939
const logs = this.activityLogs();
@@ -50,7 +50,7 @@ export class RecentActivityComponent {
5050

5151
const projectId = this.route.snapshot.params['id'] || this.route.parent?.snapshot.params['id'];
5252
if (projectId) {
53-
this.actions.getActivityLogs(projectId, pageNumber.toString(), this.pageSize().toString());
53+
this.actions.getActivityLogs(projectId, pageNumber, this.pageSize());
5454
}
5555
}
5656
}
Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,71 @@
1-
import { MockComponent } from 'ng-mocks';
1+
import { provideStore, Store } from '@ngxs/store';
22

3+
import { TranslateService } from '@ngx-translate/core';
4+
5+
import { DialogService } from 'primeng/dynamicdialog';
6+
7+
import { of } from 'rxjs';
8+
9+
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
10+
import { provideHttpClientTesting } from '@angular/common/http/testing';
311
import { ComponentFixture, TestBed } from '@angular/core/testing';
12+
import { ActivatedRoute } from '@angular/router';
13+
import { RouterTestingModule } from '@angular/router/testing';
414

5-
import { SubHeaderComponent } from '@osf/shared/components';
15+
import { DataciteService, ToastService } from '@osf/shared/services';
16+
import { GetActivityLogs } from '@shared/stores/activity-logs';
617

718
import { ProjectOverviewComponent } from './project-overview.component';
819

920
describe('ProjectOverviewComponent', () => {
10-
let component: ProjectOverviewComponent;
1121
let fixture: ComponentFixture<ProjectOverviewComponent>;
22+
let component: ProjectOverviewComponent;
23+
let store: Store;
1224

1325
beforeEach(async () => {
26+
TestBed.overrideComponent(ProjectOverviewComponent, { set: { template: '' } });
27+
1428
await TestBed.configureTestingModule({
15-
imports: [ProjectOverviewComponent, MockComponent(SubHeaderComponent)],
29+
imports: [ProjectOverviewComponent, RouterTestingModule],
30+
providers: [
31+
provideStore([]),
32+
33+
{ provide: ActivatedRoute, useValue: { snapshot: { params: { id: 'proj123' } }, parent: null } },
34+
35+
provideHttpClient(withInterceptorsFromDi()),
36+
provideHttpClientTesting(),
37+
38+
{ provide: DataciteService, useValue: {} },
39+
{ provide: DialogService, useValue: { open: () => ({ onClose: of(null) }) } },
40+
{ provide: TranslateService, useValue: { instant: (k: string) => k } },
41+
{ provide: ToastService, useValue: { showSuccess: jest.fn() } },
42+
],
1643
}).compileComponents();
1744

45+
store = TestBed.inject(Store);
1846
fixture = TestBed.createComponent(ProjectOverviewComponent);
1947
component = fixture.componentInstance;
20-
fixture.detectChanges();
2148
});
2249

2350
it('should create', () => {
2451
expect(component).toBeTruthy();
2552
});
53+
54+
it('dispatches GetActivityLogs with numeric page and pageSize on init', () => {
55+
const dispatchSpy = jest.spyOn(store, 'dispatch');
56+
57+
jest.spyOn(component as any, 'setupDataciteViewTrackerEffect').mockReturnValue(of(null));
58+
59+
component.ngOnInit();
60+
61+
const actions = dispatchSpy.mock.calls.map((c) => c[0]);
62+
const activityAction = actions.find((a) => a instanceof GetActivityLogs) as GetActivityLogs;
63+
64+
expect(activityAction).toBeDefined();
65+
expect(activityAction.projectId).toBe('proj123');
66+
expect(activityAction.page).toBe(1);
67+
expect(activityAction.pageSize).toBe(5);
68+
expect(typeof activityAction.page).toBe('number');
69+
expect(typeof activityAction.pageSize).toBe('number');
70+
});
2671
});

src/app/features/project/overview/project-overview.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ export class ProjectOverviewComponent extends DataciteTrackerComponent implement
244244
this.actions.getHomeWiki(ResourceType.Project, projectId);
245245
this.actions.getComponents(projectId);
246246
this.actions.getLinkedProjects(projectId);
247-
this.actions.getActivityLogs(projectId, this.activityDefaultPage.toString(), this.activityPageSize.toString());
247+
this.actions.getActivityLogs(projectId, this.activityDefaultPage, this.activityPageSize);
248248
this.setupDataciteViewTrackerEffect().subscribe();
249249
}
250250
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<div
2+
class="p-3 flex flex-column gap-3 border-1 surface-border border-round-xl text-color"
3+
role="region"
4+
aria-labelledby="recent-activity-title"
5+
>
6+
<h2 id="recent-activity-title" class="mb-2" data-test="recent-activity-title">
7+
{{ 'project.overview.recentActivity.title' | translate }}
8+
</h2>
9+
10+
@if (!isLoading()) {
11+
<div role="list" data-test="recent-activity-list">
12+
@for (activityLog of formattedActivityLogs(); track activityLog.id) {
13+
<div
14+
class="flex justify-content-between gap-3 pb-2 align-items-center border-bottom-1 surface-border"
15+
role="listitem"
16+
data-test="recent-activity-item"
17+
>
18+
<div [innerHTML]="activityLog.formattedActivity" data-test="recent-activity-item-content"></div>
19+
20+
<p class="hidden sm:block text-right white-space-nowrap flex-shrink-0" data-test="recent-activity-item-date">
21+
{{ activityLog.date | date: 'MMM d, y hh:mm a' }}
22+
</p>
23+
</div>
24+
} @empty {
25+
<div class="text-center text-muted-color" role="status" aria-live="polite" data-test="recent-activity-empty">
26+
{{ 'project.overview.recentActivity.noActivity' | translate }}
27+
</div>
28+
}
29+
</div>
30+
31+
@if (totalCount() > pageSize) {
32+
<osf-custom-paginator
33+
data-test="recent-activity-paginator"
34+
[showFirstLastIcon]="true"
35+
[totalCount]="totalCount()"
36+
[rows]="pageSize"
37+
[first]="firstIndex()"
38+
(pageChanged)="onPageChange($event)"
39+
/>
40+
}
41+
} @else {
42+
<div class="flex flex-column gap-2" data-test="recent-activity-skeleton">
43+
<p-skeleton width="100%" height="2rem" />
44+
<p-skeleton width="100%" height="2rem" />
45+
<p-skeleton width="100%" height="2rem" />
46+
<p-skeleton width="100%" height="2rem" />
47+
<p-skeleton width="100%" height="2rem" />
48+
</div>
49+
}
50+
</div>

0 commit comments

Comments
 (0)