Skip to content

Commit df71a30

Browse files
authored
[ENG-9728] [AOI] Ability to remove project from a collection (#816)
- Ticket: [ENG-9728] - Feature flag: n/a ## Summary of Changes 1. Added updated for project in collection. 2. Added remove from collection. 3. Added collection to metadata tab.
1 parent a4bb706 commit df71a30

File tree

50 files changed

+1100
-226
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1100
-226
lines changed

src/app/features/collections/collections.routes.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@ import { provideStates } from '@ngxs/store';
33
import { Routes } from '@angular/router';
44

55
import { authGuard } from '@core/guards/auth.guard';
6-
import { AddToCollectionState } from '@osf/features/collections/store/add-to-collection';
7-
import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation';
8-
import { ConfirmLeavingGuard } from '@shared/guards';
9-
import { BookmarksState } from '@shared/stores/bookmarks';
10-
import { CitationsState } from '@shared/stores/citations';
11-
import { CollectionsState } from '@shared/stores/collections';
12-
import { NodeLinksState } from '@shared/stores/node-links';
13-
import { ProjectsState } from '@shared/stores/projects';
14-
import { SubjectsState } from '@shared/stores/subjects';
6+
import { ConfirmLeavingGuard } from '@osf/shared/guards';
7+
import { ActivityLogsState } from '@osf/shared/stores/activity-logs';
8+
import { BookmarksState } from '@osf/shared/stores/bookmarks';
9+
import { CitationsState } from '@osf/shared/stores/citations';
10+
import { CollectionsState } from '@osf/shared/stores/collections';
11+
import { NodeLinksState } from '@osf/shared/stores/node-links';
12+
import { ProjectsState } from '@osf/shared/stores/projects';
13+
import { SubjectsState } from '@osf/shared/stores/subjects';
14+
15+
import { CollectionsModerationState } from '../moderation/store/collections-moderation';
16+
import { ProjectOverviewState } from '../project/overview/store';
17+
import { SettingsState } from '../project/settings/store';
18+
19+
import { AddToCollectionState } from './store/add-to-collection';
1520

1621
export const collectionsRoutes: Routes = [
1722
{
@@ -48,6 +53,16 @@ export const collectionsRoutes: Routes = [
4853
canActivate: [authGuard],
4954
canDeactivate: [ConfirmLeavingGuard],
5055
},
56+
{
57+
path: ':providerId/:id/edit',
58+
loadComponent: () =>
59+
import('@osf/features/collections/components/add-to-collection/add-to-collection.component').then(
60+
(mod) => mod.AddToCollectionComponent
61+
),
62+
providers: [provideStates([ProjectsState, CollectionsState, AddToCollectionState])],
63+
canActivate: [authGuard],
64+
canDeactivate: [ConfirmLeavingGuard],
65+
},
5166
{
5267
path: ':providerId/moderation',
5368
canActivate: [authGuard],
@@ -63,12 +78,15 @@ export const collectionsRoutes: Routes = [
6378
).then((mod) => mod.CollectionSubmissionOverviewComponent),
6479
providers: [
6580
provideStates([
81+
ProjectOverviewState,
6682
NodeLinksState,
6783
CitationsState,
68-
CollectionsModerationState,
6984
CollectionsState,
85+
CollectionsModerationState,
86+
ActivityLogsState,
7087
BookmarksState,
7188
SubjectsState,
89+
SettingsState,
7290
]),
7391
],
7492
},
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { MockProvider } from 'ng-mocks';
2-
31
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
42

3+
import { of, throwError } from 'rxjs';
4+
55
import { ComponentFixture, TestBed } from '@angular/core/testing';
66

77
import { ToastService } from '@osf/shared/services/toast.service';
@@ -11,88 +11,91 @@ import { AddToCollectionConfirmationDialogComponent } from './add-to-collection-
1111
import { MOCK_PROJECT } from '@testing/mocks/project.mock';
1212
import { OSFTestingModule } from '@testing/osf.testing.module';
1313
import { provideMockStore } from '@testing/providers/store-provider.mock';
14-
import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock';
1514

1615
describe('AddToCollectionConfirmationDialogComponent', () => {
1716
let component: AddToCollectionConfirmationDialogComponent;
1817
let fixture: ComponentFixture<AddToCollectionConfirmationDialogComponent>;
19-
let mockDialogRef: DynamicDialogRef;
20-
let toastServiceMock: ReturnType<ToastServiceMockBuilder['build']>;
21-
22-
const mockPayload = {
23-
collectionId: 'collection-1',
24-
projectId: 'project-1',
25-
collectionMetadata: { title: 'Test Collection' },
26-
userId: 'user-1',
27-
};
28-
29-
const mockProject = MOCK_PROJECT;
18+
let dialogRef: DynamicDialogRef;
19+
let toastService: jest.Mocked<ToastService>;
20+
let configData: { payload?: any; project?: any };
21+
let updateProjectPublicStatus: jest.Mock;
22+
let createCollectionSubmission: jest.Mock;
3023

3124
beforeEach(async () => {
32-
mockDialogRef = {
33-
close: jest.fn(),
34-
} as any;
35-
36-
toastServiceMock = ToastServiceMockBuilder.create().build();
25+
dialogRef = { close: jest.fn() } as any;
26+
toastService = { showSuccess: jest.fn() } as any;
27+
configData = {
28+
payload: {
29+
collectionId: 'collection-1',
30+
projectId: 'project-1',
31+
collectionMetadata: { title: 'Test Collection' },
32+
userId: 'user-1',
33+
},
34+
project: { ...MOCK_PROJECT, isPublic: false, id: 'project-1' },
35+
};
36+
updateProjectPublicStatus = jest.fn().mockReturnValue(of(null));
37+
createCollectionSubmission = jest.fn().mockReturnValue(of(null));
3738

3839
await TestBed.configureTestingModule({
3940
imports: [AddToCollectionConfirmationDialogComponent, OSFTestingModule],
4041
providers: [
41-
MockProvider(DynamicDialogRef, mockDialogRef),
42-
MockProvider(ToastService, toastServiceMock),
43-
MockProvider(DynamicDialogConfig, {
44-
data: {
45-
payload: mockPayload,
46-
project: mockProject,
47-
},
48-
}),
49-
provideMockStore({
50-
signals: [],
51-
}),
42+
{ provide: DynamicDialogRef, useValue: dialogRef },
43+
{ provide: ToastService, useValue: toastService },
44+
{ provide: DynamicDialogConfig, useValue: { data: configData } },
45+
provideMockStore({ signals: [] }),
5246
],
5347
}).compileComponents();
5448

5549
fixture = TestBed.createComponent(AddToCollectionConfirmationDialogComponent);
5650
component = fixture.componentInstance;
51+
component.actions = {
52+
updateProjectPublicStatus,
53+
createCollectionSubmission,
54+
} as any;
5755
fixture.detectChanges();
5856
});
5957

6058
it('should create', () => {
6159
expect(component).toBeTruthy();
6260
});
6361

64-
it('should initialize with dialog data', () => {
65-
expect(component.config.data.payload).toEqual(mockPayload);
66-
expect(component.config.data.project).toEqual(mockProject);
62+
it('should dispatch updates and close on confirm when project is private', () => {
63+
component.handleAddToCollectionConfirm();
64+
65+
expect(updateProjectPublicStatus).toHaveBeenCalledWith([{ id: 'project-1', public: true }]);
66+
expect(createCollectionSubmission).toHaveBeenCalledWith(configData.payload);
67+
expect(dialogRef.close).toHaveBeenCalledWith(true);
68+
expect(toastService.showSuccess).toHaveBeenCalledWith('collections.addToCollection.confirmationDialogToastMessage');
69+
expect(component.isSubmitting()).toBe(false);
6770
});
6871

69-
it('should handle add to collection confirmation', () => {
72+
it('should skip public status update when project already public', () => {
73+
configData.project.isPublic = true;
74+
updateProjectPublicStatus.mockClear();
75+
7076
component.handleAddToCollectionConfirm();
7177

72-
expect(mockDialogRef.close).toHaveBeenCalledWith(true);
78+
expect(updateProjectPublicStatus).not.toHaveBeenCalled();
79+
expect(createCollectionSubmission).toHaveBeenCalledWith(configData.payload);
80+
expect(dialogRef.close).toHaveBeenCalledWith(true);
7381
});
7482

75-
it('should have config data', () => {
76-
expect(component.config.data.payload).toBeDefined();
77-
expect(component.config.data.payload.collectionId).toBe('collection-1');
78-
expect(component.config.data.payload.projectId).toBe('project-1');
79-
expect(component.config.data.payload.userId).toBe('user-1');
80-
});
83+
it('should do nothing when payload or project is missing', () => {
84+
configData.payload = undefined;
85+
component.handleAddToCollectionConfirm();
8186

82-
it('should have project data in config', () => {
83-
expect(component.config.data.project).toBeDefined();
84-
expect(component.config.data.project.id).toBe('project-1');
85-
expect(component.config.data.project.title).toBe('Test Project');
86-
expect(component.config.data.project.isPublic).toBe(true);
87+
expect(updateProjectPublicStatus).not.toHaveBeenCalled();
88+
expect(createCollectionSubmission).not.toHaveBeenCalled();
89+
expect(dialogRef.close).not.toHaveBeenCalled();
8790
});
8891

89-
it('should have actions defined', () => {
90-
expect(component.actions).toBeDefined();
91-
expect(component.actions.createCollectionSubmission).toBeDefined();
92-
expect(component.actions.updateProjectPublicStatus).toBeDefined();
93-
});
92+
it('should reset submitting state and not close on error', () => {
93+
createCollectionSubmission.mockReturnValue(throwError(() => new Error('fail')));
94+
95+
component.handleAddToCollectionConfirm();
9496

95-
it('should have isSubmitting signal', () => {
9697
expect(component.isSubmitting()).toBe(false);
98+
expect(dialogRef.close).not.toHaveBeenCalled();
99+
expect(toastService.showSuccess).not.toHaveBeenCalled();
97100
});
98101
});

src/app/features/collections/components/add-to-collection/add-to-collection.component.html

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ <h1 class="collections-heading flex align-items-center">{{ collectionProvider()?
1414

1515
<div class="content-container flex-1">
1616
<p-stepper class="collection-stepper flex flex-column gap-5" [value]="stepperActiveValue()">
17-
<osf-select-project-step
18-
[stepperActiveValue]="stepperActiveValue()"
19-
[collectionId]="primaryCollectionId() ?? ''"
20-
[targetStepValue]="AddToCollectionSteps.SelectProject"
21-
(projectSelected)="handleProjectSelected()"
22-
(stepChange)="handleChangeStep($event)"
23-
/>
17+
@if (!isEditMode()) {
18+
<osf-select-project-step
19+
[stepperActiveValue]="stepperActiveValue()"
20+
[collectionId]="primaryCollectionId() ?? ''"
21+
[targetStepValue]="AddToCollectionSteps.SelectProject"
22+
(projectSelected)="handleProjectSelected()"
23+
(stepChange)="handleChangeStep($event)"
24+
/>
25+
}
2426

2527
<osf-project-metadata-step
2628
[stepperActiveValue]="stepperActiveValue()"
@@ -51,13 +53,24 @@ <h1 class="collections-heading flex align-items-center">{{ collectionProvider()?
5153
/>
5254
</p-stepper>
5355

54-
<div class="flex justify-content-end gap-3 mt-4">
55-
<p-button severity="info" [label]="'common.buttons.cancel' | translate" [routerLink]="['../']" />
56-
<p-button
57-
[label]="'common.buttons.addToCollection' | translate"
58-
[disabled]="stepperActiveValue() !== AddToCollectionSteps.Complete"
59-
(click)="handleAddToCollection()"
60-
/>
56+
<div class="flex justify-content-end gap-3 mt-4" [class.justify-content-between]="showRemoveButton()">
57+
@if (showRemoveButton()) {
58+
<p-button
59+
severity="danger"
60+
[label]="'common.buttons.removeFromCollection' | translate"
61+
(click)="handleRemoveFromCollection()"
62+
/>
63+
}
64+
65+
<div class="flex align-items-center gap-3">
66+
<p-button severity="info" [label]="'common.buttons.cancel' | translate" [routerLink]="['../']" />
67+
68+
<p-button
69+
[label]="(isEditMode() ? 'common.buttons.update' : 'common.buttons.addToCollection') | translate"
70+
[disabled]="stepperActiveValue() !== AddToCollectionSteps.Complete"
71+
(click)="handleAddToCollection()"
72+
/>
73+
</div>
6174
</div>
6275
</div>
6376
</section>

src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('AddToCollectionComponent', () => {
3737

3838
beforeEach(async () => {
3939
mockRouter = RouterMockBuilder.create().build();
40-
mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ id: '1' }).build();
40+
mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ id: null }).build();
4141
mockCustomDialogService = CustomDialogServiceMockBuilder.create().build();
4242

4343
await TestBed.configureTestingModule({

0 commit comments

Comments
 (0)