Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions src/app/features/collections/collections.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import { provideStates } from '@ngxs/store';
import { Routes } from '@angular/router';

import { authGuard } from '@core/guards/auth.guard';
import { AddToCollectionState } from '@osf/features/collections/store/add-to-collection';
import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation';
import { ConfirmLeavingGuard } from '@shared/guards';
import { BookmarksState } from '@shared/stores/bookmarks';
import { CitationsState } from '@shared/stores/citations';
import { CollectionsState } from '@shared/stores/collections';
import { NodeLinksState } from '@shared/stores/node-links';
import { ProjectsState } from '@shared/stores/projects';
import { SubjectsState } from '@shared/stores/subjects';
import { ConfirmLeavingGuard } from '@osf/shared/guards';
import { ActivityLogsState } from '@osf/shared/stores/activity-logs';
import { BookmarksState } from '@osf/shared/stores/bookmarks';
import { CitationsState } from '@osf/shared/stores/citations';
import { CollectionsState } from '@osf/shared/stores/collections';
import { NodeLinksState } from '@osf/shared/stores/node-links';
import { ProjectsState } from '@osf/shared/stores/projects';
import { SubjectsState } from '@osf/shared/stores/subjects';

import { CollectionsModerationState } from '../moderation/store/collections-moderation';
import { ProjectOverviewState } from '../project/overview/store';
import { SettingsState } from '../project/settings/store';

import { AddToCollectionState } from './store/add-to-collection';

export const collectionsRoutes: Routes = [
{
Expand Down Expand Up @@ -48,6 +53,16 @@ export const collectionsRoutes: Routes = [
canActivate: [authGuard],
canDeactivate: [ConfirmLeavingGuard],
},
{
path: ':providerId/:id/edit',
loadComponent: () =>
import('@osf/features/collections/components/add-to-collection/add-to-collection.component').then(
(mod) => mod.AddToCollectionComponent
),
providers: [provideStates([ProjectsState, CollectionsState, AddToCollectionState])],
canActivate: [authGuard],
canDeactivate: [ConfirmLeavingGuard],
},
{
path: ':providerId/moderation',
canActivate: [authGuard],
Expand All @@ -63,12 +78,15 @@ export const collectionsRoutes: Routes = [
).then((mod) => mod.CollectionSubmissionOverviewComponent),
providers: [
provideStates([
ProjectOverviewState,
NodeLinksState,
CitationsState,
CollectionsModerationState,
CollectionsState,
CollectionsModerationState,
ActivityLogsState,
BookmarksState,
SubjectsState,
SettingsState,
]),
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MockProvider } from 'ng-mocks';

import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';

import { of, throwError } from 'rxjs';

import { ComponentFixture, TestBed } from '@angular/core/testing';

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

describe('AddToCollectionConfirmationDialogComponent', () => {
let component: AddToCollectionConfirmationDialogComponent;
let fixture: ComponentFixture<AddToCollectionConfirmationDialogComponent>;
let mockDialogRef: DynamicDialogRef;
let toastServiceMock: ReturnType<ToastServiceMockBuilder['build']>;

const mockPayload = {
collectionId: 'collection-1',
projectId: 'project-1',
collectionMetadata: { title: 'Test Collection' },
userId: 'user-1',
};

const mockProject = MOCK_PROJECT;
let dialogRef: DynamicDialogRef;
let toastService: jest.Mocked<ToastService>;
let configData: { payload?: any; project?: any };
let updateProjectPublicStatus: jest.Mock;
let createCollectionSubmission: jest.Mock;

beforeEach(async () => {
mockDialogRef = {
close: jest.fn(),
} as any;

toastServiceMock = ToastServiceMockBuilder.create().build();
dialogRef = { close: jest.fn() } as any;
toastService = { showSuccess: jest.fn() } as any;
configData = {
payload: {
collectionId: 'collection-1',
projectId: 'project-1',
collectionMetadata: { title: 'Test Collection' },
userId: 'user-1',
},
project: { ...MOCK_PROJECT, isPublic: false, id: 'project-1' },
};
updateProjectPublicStatus = jest.fn().mockReturnValue(of(null));
createCollectionSubmission = jest.fn().mockReturnValue(of(null));

await TestBed.configureTestingModule({
imports: [AddToCollectionConfirmationDialogComponent, OSFTestingModule],
providers: [
MockProvider(DynamicDialogRef, mockDialogRef),
MockProvider(ToastService, toastServiceMock),
MockProvider(DynamicDialogConfig, {
data: {
payload: mockPayload,
project: mockProject,
},
}),
provideMockStore({
signals: [],
}),
{ provide: DynamicDialogRef, useValue: dialogRef },
{ provide: ToastService, useValue: toastService },
{ provide: DynamicDialogConfig, useValue: { data: configData } },
provideMockStore({ signals: [] }),
],
}).compileComponents();

fixture = TestBed.createComponent(AddToCollectionConfirmationDialogComponent);
component = fixture.componentInstance;
component.actions = {
updateProjectPublicStatus,
createCollectionSubmission,
} as any;
fixture.detectChanges();
});

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

it('should initialize with dialog data', () => {
expect(component.config.data.payload).toEqual(mockPayload);
expect(component.config.data.project).toEqual(mockProject);
it('should dispatch updates and close on confirm when project is private', () => {
component.handleAddToCollectionConfirm();

expect(updateProjectPublicStatus).toHaveBeenCalledWith([{ id: 'project-1', public: true }]);
expect(createCollectionSubmission).toHaveBeenCalledWith(configData.payload);
expect(dialogRef.close).toHaveBeenCalledWith(true);
expect(toastService.showSuccess).toHaveBeenCalledWith('collections.addToCollection.confirmationDialogToastMessage');
expect(component.isSubmitting()).toBe(false);
});

it('should handle add to collection confirmation', () => {
it('should skip public status update when project already public', () => {
configData.project.isPublic = true;
updateProjectPublicStatus.mockClear();

component.handleAddToCollectionConfirm();

expect(mockDialogRef.close).toHaveBeenCalledWith(true);
expect(updateProjectPublicStatus).not.toHaveBeenCalled();
expect(createCollectionSubmission).toHaveBeenCalledWith(configData.payload);
expect(dialogRef.close).toHaveBeenCalledWith(true);
});

it('should have config data', () => {
expect(component.config.data.payload).toBeDefined();
expect(component.config.data.payload.collectionId).toBe('collection-1');
expect(component.config.data.payload.projectId).toBe('project-1');
expect(component.config.data.payload.userId).toBe('user-1');
});
it('should do nothing when payload or project is missing', () => {
configData.payload = undefined;
component.handleAddToCollectionConfirm();

it('should have project data in config', () => {
expect(component.config.data.project).toBeDefined();
expect(component.config.data.project.id).toBe('project-1');
expect(component.config.data.project.title).toBe('Test Project');
expect(component.config.data.project.isPublic).toBe(true);
expect(updateProjectPublicStatus).not.toHaveBeenCalled();
expect(createCollectionSubmission).not.toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});

it('should have actions defined', () => {
expect(component.actions).toBeDefined();
expect(component.actions.createCollectionSubmission).toBeDefined();
expect(component.actions.updateProjectPublicStatus).toBeDefined();
});
it('should reset submitting state and not close on error', () => {
createCollectionSubmission.mockReturnValue(throwError(() => new Error('fail')));

component.handleAddToCollectionConfirm();

it('should have isSubmitting signal', () => {
expect(component.isSubmitting()).toBe(false);
expect(dialogRef.close).not.toHaveBeenCalled();
expect(toastService.showSuccess).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ <h1 class="collections-heading flex align-items-center">{{ collectionProvider()?

<div class="content-container flex-1">
<p-stepper class="collection-stepper flex flex-column gap-5" [value]="stepperActiveValue()">
<osf-select-project-step
[stepperActiveValue]="stepperActiveValue()"
[collectionId]="primaryCollectionId() ?? ''"
[targetStepValue]="AddToCollectionSteps.SelectProject"
(projectSelected)="handleProjectSelected()"
(stepChange)="handleChangeStep($event)"
/>
@if (!isEditMode()) {
<osf-select-project-step
[stepperActiveValue]="stepperActiveValue()"
[collectionId]="primaryCollectionId() ?? ''"
[targetStepValue]="AddToCollectionSteps.SelectProject"
(projectSelected)="handleProjectSelected()"
(stepChange)="handleChangeStep($event)"
/>
}

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

<div class="flex justify-content-end gap-3 mt-4">
<p-button severity="info" [label]="'common.buttons.cancel' | translate" [routerLink]="['../']" />
<p-button
[label]="'common.buttons.addToCollection' | translate"
[disabled]="stepperActiveValue() !== AddToCollectionSteps.Complete"
(click)="handleAddToCollection()"
/>
<div class="flex justify-content-end gap-3 mt-4" [class.justify-content-between]="showRemoveButton()">
@if (showRemoveButton()) {
<p-button
severity="danger"
[label]="'common.buttons.removeFromCollection' | translate"
(click)="handleRemoveFromCollection()"
/>
}

<div class="flex align-items-center gap-3">
<p-button severity="info" [label]="'common.buttons.cancel' | translate" [routerLink]="['../']" />

<p-button
[label]="(isEditMode() ? 'common.buttons.update' : 'common.buttons.addToCollection') | translate"
[disabled]="stepperActiveValue() !== AddToCollectionSteps.Complete"
(click)="handleAddToCollection()"
/>
</div>
</div>
</div>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('AddToCollectionComponent', () => {

beforeEach(async () => {
mockRouter = RouterMockBuilder.create().build();
mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ id: '1' }).build();
mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ id: null }).build();
mockCustomDialogService = CustomDialogServiceMockBuilder.create().build();

await TestBed.configureTestingModule({
Expand Down
Loading