Skip to content

Commit 22f687d

Browse files
authored
[ENG-9898] Recent activity improvements (#800)
- Ticket: [ENG-9898] - Feature flag: n/a ## Purpose Make it reusable and maintainable. ## Summary of Changes 1. Improved recent activity.
1 parent 93c6466 commit 22f687d

29 files changed

+993
-591
lines changed

src/app/features/files/pages/files/files.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export class FilesComponent {
261261
);
262262

263263
constructor() {
264-
this.activeRoute.parent?.parent?.parent?.params.subscribe((params) => {
264+
this.activeRoute.parent?.parent?.parent?.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
265265
if (params['id']) {
266266
this.resourceId.set(params['id']);
267267
}

src/app/features/project/overview/components/files-widget/files-widget.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ <h2>{{ 'project.overview.files.filesPreview' | translate }}</h2>
1818
<p-tabs [value]="currentRootFolder()?.folder?.id || ''" (valueChange)="onStorageChange($event)" scrollable>
1919
<p-tablist>
2020
@for (option of storageAddons(); track option.folder.id) {
21-
<p-tab class="p-1" [value]="option.folder.id">
21+
<p-tab class="p-2" [value]="option.folder.id">
2222
<p-button
2323
severity="contrast"
2424
variant="text"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<div class="flex flex-column gap-3 border-1 surface-border border-round-xl p-2">
2+
<h2 class="mb-2 px-3 pt-3" data-test="recent-activity-title">
3+
{{ 'project.overview.recentActivity.title' | translate }}
4+
</h2>
5+
6+
<osf-recent-activity-list
7+
[activityLogs]="activityLogs()"
8+
[isLoading]="isLoading()"
9+
[totalCount]="totalCount()"
10+
[pageSize]="pageSize()"
11+
[firstIndex]="firstIndex()"
12+
(pageChange)="onPageChange($event)"
13+
/>
14+
</div>
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { Store } from '@ngxs/store';
2+
3+
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
5+
import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum';
6+
import { ActivityLogsSelectors, ClearActivityLogs } from '@osf/shared/stores/activity-logs';
7+
8+
import { ProjectRecentActivityComponent } from './project-recent-activity.component';
9+
10+
import { MOCK_ACTIVITY_LOGS_WITH_DISPLAY } from '@testing/mocks/activity-log-with-display.mock';
11+
import { OSFTestingModule } from '@testing/osf.testing.module';
12+
import { provideMockStore } from '@testing/providers/store-provider.mock';
13+
14+
describe('ProjectRecentActivityComponent', () => {
15+
let component: ProjectRecentActivityComponent;
16+
let fixture: ComponentFixture<ProjectRecentActivityComponent>;
17+
let store: Store;
18+
19+
beforeEach(async () => {
20+
await TestBed.configureTestingModule({
21+
imports: [ProjectRecentActivityComponent, OSFTestingModule],
22+
providers: [
23+
provideMockStore({
24+
signals: [
25+
{ selector: ActivityLogsSelectors.getActivityLogs, value: [] },
26+
{ selector: ActivityLogsSelectors.getActivityLogsTotalCount, value: 0 },
27+
{ selector: ActivityLogsSelectors.getActivityLogsLoading, value: false },
28+
],
29+
}),
30+
],
31+
}).compileComponents();
32+
33+
store = TestBed.inject(Store);
34+
jest.spyOn(store, 'dispatch');
35+
36+
fixture = TestBed.createComponent(ProjectRecentActivityComponent);
37+
component = fixture.componentInstance;
38+
});
39+
40+
it('should initialize with default values', () => {
41+
expect(component.pageSize()).toBe(5);
42+
expect(component.currentPage()).toBe(1);
43+
expect(component.firstIndex()).toBe(0);
44+
});
45+
46+
it('should dispatch GetActivityLogs when projectId is provided', () => {
47+
fixture.componentRef.setInput('projectId', 'project123');
48+
fixture.detectChanges();
49+
50+
expect(store.dispatch).toHaveBeenCalledWith(
51+
expect.objectContaining({
52+
resourceId: 'project123',
53+
resourceType: CurrentResourceType.Projects,
54+
page: 1,
55+
pageSize: 5,
56+
})
57+
);
58+
});
59+
60+
it('should not dispatch when projectId is not provided', () => {
61+
fixture.detectChanges();
62+
63+
expect(store.dispatch).not.toHaveBeenCalled();
64+
});
65+
66+
it('should dispatch GetActivityLogs when currentPage changes', () => {
67+
fixture.componentRef.setInput('projectId', 'project123');
68+
fixture.detectChanges();
69+
70+
(store.dispatch as jest.Mock).mockClear();
71+
72+
component.currentPage.set(2);
73+
fixture.detectChanges();
74+
75+
expect(store.dispatch).toHaveBeenCalledWith(
76+
expect.objectContaining({
77+
resourceId: 'project123',
78+
resourceType: CurrentResourceType.Projects,
79+
page: 2,
80+
pageSize: 5,
81+
})
82+
);
83+
});
84+
85+
it('should update currentPage and dispatch on page change', () => {
86+
fixture.componentRef.setInput('projectId', 'project123');
87+
fixture.detectChanges();
88+
89+
(store.dispatch as jest.Mock).mockClear();
90+
91+
component.onPageChange({ page: 1 } as any);
92+
fixture.detectChanges();
93+
94+
expect(component.currentPage()).toBe(2);
95+
expect(store.dispatch).toHaveBeenCalledWith(
96+
expect.objectContaining({
97+
page: 2,
98+
})
99+
);
100+
});
101+
102+
it('should not update currentPage when page is undefined', () => {
103+
fixture.componentRef.setInput('projectId', 'project123');
104+
fixture.detectChanges();
105+
106+
const initialPage = component.currentPage();
107+
component.onPageChange({} as any);
108+
109+
expect(component.currentPage()).toBe(initialPage);
110+
});
111+
112+
it('should compute firstIndex correctly', () => {
113+
component.currentPage.set(1);
114+
expect(component.firstIndex()).toBe(0);
115+
116+
component.currentPage.set(2);
117+
expect(component.firstIndex()).toBe(5);
118+
119+
component.currentPage.set(3);
120+
expect(component.firstIndex()).toBe(10);
121+
});
122+
123+
it('should clear store on destroy', () => {
124+
fixture.componentRef.setInput('projectId', 'project123');
125+
fixture.detectChanges();
126+
127+
(store.dispatch as jest.Mock).mockClear();
128+
129+
fixture.destroy();
130+
131+
expect(store.dispatch).toHaveBeenCalledWith(expect.any(ClearActivityLogs));
132+
});
133+
134+
it('should return activity logs from selector', () => {
135+
TestBed.resetTestingModule();
136+
TestBed.configureTestingModule({
137+
imports: [ProjectRecentActivityComponent, OSFTestingModule],
138+
providers: [
139+
provideMockStore({
140+
signals: [
141+
{ selector: ActivityLogsSelectors.getActivityLogs, value: MOCK_ACTIVITY_LOGS_WITH_DISPLAY },
142+
{ selector: ActivityLogsSelectors.getActivityLogsTotalCount, value: 2 },
143+
{ selector: ActivityLogsSelectors.getActivityLogsLoading, value: false },
144+
],
145+
}),
146+
],
147+
}).compileComponents();
148+
149+
fixture = TestBed.createComponent(ProjectRecentActivityComponent);
150+
component = fixture.componentInstance;
151+
fixture.detectChanges();
152+
153+
expect(component.activityLogs()).toEqual(MOCK_ACTIVITY_LOGS_WITH_DISPLAY);
154+
});
155+
156+
it('should return totalCount from selector', () => {
157+
TestBed.resetTestingModule();
158+
TestBed.configureTestingModule({
159+
imports: [ProjectRecentActivityComponent, OSFTestingModule],
160+
providers: [
161+
provideMockStore({
162+
signals: [
163+
{ selector: ActivityLogsSelectors.getActivityLogs, value: [] },
164+
{ selector: ActivityLogsSelectors.getActivityLogsTotalCount, value: 10 },
165+
{ selector: ActivityLogsSelectors.getActivityLogsLoading, value: false },
166+
],
167+
}),
168+
],
169+
}).compileComponents();
170+
171+
fixture = TestBed.createComponent(ProjectRecentActivityComponent);
172+
component = fixture.componentInstance;
173+
fixture.detectChanges();
174+
175+
expect(component.totalCount()).toBe(10);
176+
});
177+
178+
it('should return isLoading from selector', () => {
179+
TestBed.resetTestingModule();
180+
TestBed.configureTestingModule({
181+
imports: [ProjectRecentActivityComponent, OSFTestingModule],
182+
providers: [
183+
provideMockStore({
184+
signals: [
185+
{ selector: ActivityLogsSelectors.getActivityLogs, value: [] },
186+
{ selector: ActivityLogsSelectors.getActivityLogsTotalCount, value: 0 },
187+
{ selector: ActivityLogsSelectors.getActivityLogsLoading, value: true },
188+
],
189+
}),
190+
],
191+
}).compileComponents();
192+
193+
fixture = TestBed.createComponent(ProjectRecentActivityComponent);
194+
component = fixture.componentInstance;
195+
fixture.detectChanges();
196+
197+
expect(component.isLoading()).toBe(true);
198+
});
199+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createDispatchMap, select } from '@ngxs/store';
2+
3+
import { TranslatePipe } from '@ngx-translate/core';
4+
5+
import { PaginatorState } from 'primeng/paginator';
6+
7+
import { ChangeDetectionStrategy, Component, computed, effect, input, OnDestroy, signal } from '@angular/core';
8+
9+
import { RecentActivityListComponent } from '@osf/shared/components/recent-activity/recent-activity-list.component';
10+
import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum';
11+
import { ActivityLogsSelectors, ClearActivityLogs, GetActivityLogs } from '@osf/shared/stores/activity-logs';
12+
13+
@Component({
14+
selector: 'osf-project-recent-activity',
15+
imports: [RecentActivityListComponent, TranslatePipe],
16+
templateUrl: './project-recent-activity.component.html',
17+
changeDetection: ChangeDetectionStrategy.OnPush,
18+
})
19+
export class ProjectRecentActivityComponent implements OnDestroy {
20+
projectId = input<string>();
21+
22+
pageSize = signal(5);
23+
currentPage = signal<number>(1);
24+
25+
activityLogs = select(ActivityLogsSelectors.getActivityLogs);
26+
totalCount = select(ActivityLogsSelectors.getActivityLogsTotalCount);
27+
isLoading = select(ActivityLogsSelectors.getActivityLogsLoading);
28+
29+
actions = createDispatchMap({ getActivityLogs: GetActivityLogs, clearActivityLogsStore: ClearActivityLogs });
30+
31+
firstIndex = computed(() => (this.currentPage() - 1) * this.pageSize());
32+
33+
constructor() {
34+
effect(() => {
35+
const projectId = this.projectId();
36+
const page = this.currentPage();
37+
38+
if (projectId) {
39+
this.actions.getActivityLogs(projectId, CurrentResourceType.Projects, page, this.pageSize());
40+
}
41+
});
42+
}
43+
44+
ngOnDestroy(): void {
45+
this.actions.clearActivityLogsStore();
46+
}
47+
48+
onPageChange(event: PaginatorState) {
49+
if (event.page !== undefined) {
50+
const pageNumber = event.page + 1;
51+
this.currentPage.set(pageNumber);
52+
}
53+
}
54+
}

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

Lines changed: 0 additions & 36 deletions
This file was deleted.

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

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)