diff --git a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html index be4fd3586..d9a515f3e 100644 --- a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html +++ b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html @@ -39,7 +39,7 @@

{{ 'collections.addToCollection.projectContributors' | translate }}

diff --git a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts index 56c28c377..339357af7 100644 --- a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts +++ b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts @@ -6,7 +6,6 @@ import { Button } from 'primeng/button'; import { Step, StepItem, StepPanel } from 'primeng/stepper'; import { Tooltip } from 'primeng/tooltip'; -import { forkJoin } from 'rxjs'; import { filter } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core'; @@ -24,10 +23,11 @@ import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models' import { CustomConfirmationService, CustomDialogService, ToastService } from '@osf/shared/services'; import { AddContributor, + BulkAddContributors, + BulkUpdateContributors, ContributorsSelectors, DeleteContributor, ProjectsSelectors, - UpdateContributor, } from '@osf/shared/stores'; @Component({ @@ -43,11 +43,11 @@ export class ProjectContributorsStepComponent { private readonly toastService = inject(ToastService); private readonly customConfirmationService = inject(CustomConfirmationService); - readonly projectContributors = select(ContributorsSelectors.getContributors); readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); readonly selectedProject = select(ProjectsSelectors.getSelectedProject); - private initialContributors = signal([]); + private initialContributors = select(ContributorsSelectors.getContributors); + readonly projectContributors = signal([]); stepperActiveValue = input.required(); targetStepValue = input.required(); @@ -59,7 +59,8 @@ export class ProjectContributorsStepComponent { actions = createDispatchMap({ addContributor: AddContributor, - updateContributor: UpdateContributor, + bulkAddContributors: BulkAddContributors, + bulkUpdateContributors: BulkUpdateContributors, deleteContributor: DeleteContributor, }); @@ -100,17 +101,16 @@ export class ProjectContributorsStepComponent { const updatedContributors = findChangedItems(this.initialContributors(), this.projectContributors(), 'id'); if (!updatedContributors.length) { - this.initialContributors.set(JSON.parse(JSON.stringify(this.projectContributors()))); + this.projectContributors.set(JSON.parse(JSON.stringify(this.initialContributors()))); this.contributorsSaved.emit(); } else { - const updateRequests = updatedContributors.map((payload) => - this.actions.updateContributor(this.selectedProject()?.id, ResourceType.Project, payload) - ); - forkJoin(updateRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); - this.initialContributors.set(JSON.parse(JSON.stringify(this.projectContributors()))); - this.contributorsSaved.emit(); - }); + this.actions + .bulkUpdateContributors(this.selectedProject()?.id, ResourceType.Project, updatedContributors) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); + this.contributorsSaved.emit(); + }); } } else { this.contributorsSaved.emit(); @@ -138,13 +138,12 @@ export class ProjectContributorsStepComponent { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => - this.actions.addContributor(this.selectedProject()?.id, ResourceType.Project, payload) - ); - - forkJoin(addRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); - }); + this.actions + .bulkAddContributors(this.selectedProject()?.id, ResourceType.Project, res.data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage') + ); } }); } @@ -175,10 +174,10 @@ export class ProjectContributorsStepComponent { private setupEffects(): void { effect(() => { const isMetadataSaved = this.isProjectMetadataSaved(); - const contributors = this.projectContributors(); + const contributors = this.initialContributors(); - if (isMetadataSaved && contributors.length && !this.initialContributors().length) { - this.initialContributors.set(JSON.parse(JSON.stringify(contributors))); + if (isMetadataSaved && contributors.length) { + this.projectContributors.set(JSON.parse(JSON.stringify(contributors))); } }); } diff --git a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html index 6b4f2d8ea..fa13b3da4 100644 --- a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html +++ b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html @@ -16,7 +16,7 @@ - this.actions.addContributor(this.resourceId, this.resourceType, payload) - ); - - forkJoin(addRequests).subscribe(() => - this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage') - ); + this.actions + .bulkAddContributors(this.resourceId, this.resourceType, res.data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage') + ); } } }); @@ -199,12 +200,11 @@ export class ContributorsDialogComponent implements OnInit { onSave(): void { const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); - const updateRequests = updatedContributors.map((payload) => - this.actions.updateContributor(this.resourceId, this.resourceType, payload) - ); - - forkJoin(updateRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); - }); + this.actions + .bulkUpdateContributors(this.resourceId, this.resourceType, updatedContributors) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage') + ); } } diff --git a/src/app/features/preprints/components/index.ts b/src/app/features/preprints/components/index.ts index f8f1fb1dc..3c13eac87 100644 --- a/src/app/features/preprints/components/index.ts +++ b/src/app/features/preprints/components/index.ts @@ -11,11 +11,11 @@ export { PreprintProviderHeroComponent } from './preprint-provider-hero/preprint export { PreprintServicesComponent } from './preprint-services/preprint-services.component'; export { PreprintsHelpDialogComponent } from './preprints-help-dialog/preprints-help-dialog.component'; export { AuthorAssertionsStepComponent } from './stepper/author-assertion-step/author-assertions-step.component'; +export { PreprintsMetadataStepComponent } from './stepper/preprints-metadata-step/preprints-metadata-step.component'; export { SupplementsStepComponent } from './stepper/supplements-step/supplements-step.component'; export { MakeDecisionComponent } from '@osf/features/preprints/components/preprint-details/make-decision/make-decision.component'; export { PreprintTombstoneComponent } from '@osf/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component'; export { WithdrawDialogComponent } from '@osf/features/preprints/components/preprint-details/withdraw-dialog/withdraw-dialog.component'; export { FileStepComponent } from '@osf/features/preprints/components/stepper/file-step/file-step.component'; -export { MetadataStepComponent } from '@osf/features/preprints/components/stepper/metadata-step/metadata-step.component'; export { ReviewStepComponent } from '@osf/features/preprints/components/stepper/review-step/review-step.component'; export { TitleAndAbstractStepComponent } from '@osf/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component'; diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.html b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.html similarity index 100% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.html rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.html diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.scss b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.scss similarity index 100% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.scss rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.scss diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts similarity index 100% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.spec.ts diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.ts similarity index 91% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.ts rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.ts index 5941b7ca4..1d456a86b 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-affiliated-institutions/preprints-affiliated-institutions.component.ts @@ -7,15 +7,15 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, effect, input, OnInit, signal } from '@angular/core'; import { PreprintProviderDetails } from '@osf/features/preprints/models'; -import { AffiliatedInstitutionSelectComponent } from '@shared/components'; -import { ResourceType } from '@shared/enums'; -import { Institution } from '@shared/models'; +import { AffiliatedInstitutionSelectComponent } from '@osf/shared/components'; +import { ResourceType } from '@osf/shared/enums'; +import { Institution } from '@osf/shared/models'; import { FetchResourceInstitutions, FetchUserInstitutions, InstitutionsSelectors, UpdateResourceInstitutions, -} from '@shared/stores/institutions'; +} from '@osf/shared/stores/institutions'; @Component({ selector: 'osf-preprints-affiliated-institutions', diff --git a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.html b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.html similarity index 96% rename from src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.html rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.html index 046e8821e..32c42f1ab 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.html +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.html @@ -9,7 +9,7 @@

{{ 'project.overview.metadata.contributors' | translate }}

{ - let component: ContributorsComponent; - let fixture: ComponentFixture; +describe('PreprintsContributorsComponent', () => { + let component: PreprintsContributorsComponent; + let fixture: ComponentFixture; let toastServiceMock: ReturnType; let confirmationServiceMock: ReturnType; let mockCustomDialogService: ReturnType; @@ -33,7 +33,7 @@ describe('ContributorsComponent', () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ContributorsComponent, OSFTestingModule, MockComponent(ContributorsTableComponent)], + imports: [PreprintsContributorsComponent, OSFTestingModule, MockComponent(ContributorsTableComponent)], providers: [ MockProvider(ToastService, toastServiceMock), MockProvider(CustomConfirmationService, confirmationServiceMock), @@ -57,7 +57,7 @@ describe('ContributorsComponent', () => { ], }).compileComponents(); - fixture = TestBed.createComponent(ContributorsComponent); + fixture = TestBed.createComponent(PreprintsContributorsComponent); component = fixture.componentInstance; fixture.componentRef.setInput('preprintId', 'preprint-1'); }); diff --git a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.ts similarity index 81% rename from src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.ts index 507a0f494..0309f34bb 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.ts @@ -7,7 +7,7 @@ import { Card } from 'primeng/card'; import { Message } from 'primeng/message'; import { TableModule } from 'primeng/table'; -import { filter, forkJoin } from 'rxjs'; +import { filter } from 'rxjs'; import { ChangeDetectionStrategy, @@ -35,20 +35,21 @@ import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models' import { CustomConfirmationService, CustomDialogService, ToastService } from '@osf/shared/services'; import { AddContributor, + BulkAddContributors, + BulkUpdateContributors, ContributorsSelectors, DeleteContributor, GetAllContributors, - UpdateContributor, } from '@osf/shared/stores'; @Component({ - selector: 'osf-preprint-contributors', + selector: 'osf-preprints-contributors', imports: [FormsModule, TableModule, ContributorsTableComponent, TranslatePipe, Card, Button, Message], - templateUrl: './contributors.component.html', - styleUrl: './contributors.component.scss', + templateUrl: './preprints-contributors.component.html', + styleUrl: './preprints-contributors.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ContributorsComponent implements OnInit { +export class PreprintsContributorsComponent implements OnInit { preprintId = input(''); readonly destroyRef = inject(DestroyRef); @@ -66,15 +67,17 @@ export class ContributorsComponent implements OnInit { const initialContributors = this.initialContributors(); if (!currentUserId) return false; - return initialContributors.some((contributor: ContributorModel) => { - return contributor.userId === currentUserId && contributor.permission === ContributorPermission.Admin; - }); + return initialContributors.some( + (contributor: ContributorModel) => + contributor.userId === currentUserId && contributor.permission === ContributorPermission.Admin + ); }); actions = createDispatchMap({ getContributors: GetAllContributors, deleteContributor: DeleteContributor, - updateContributor: UpdateContributor, + bulkUpdateContributors: BulkUpdateContributors, + bulkAddContributors: BulkAddContributors, addContributor: AddContributor, }); @@ -99,13 +102,12 @@ export class ContributorsComponent implements OnInit { save() { const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); - const updateRequests = updatedContributors.map((payload) => - this.actions.updateContributor(this.preprintId(), ResourceType.Preprint, payload) - ); - - forkJoin(updateRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); - }); + this.actions + .bulkUpdateContributors(this.preprintId(), ResourceType.Preprint, updatedContributors) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage') + ); } openAddContributorDialog() { @@ -125,13 +127,12 @@ export class ContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => - this.actions.addContributor(this.preprintId(), ResourceType.Preprint, payload) - ); - - forkJoin(addRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); - }); + this.actions + .bulkAddContributors(this.preprintId(), ResourceType.Preprint, res.data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage') + ); } }); } diff --git a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.html similarity index 98% rename from src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.html index 7bbba2f5b..dec11eca6 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.html @@ -1,7 +1,7 @@

{{ 'preprints.preprintStepper.metadata.title' | translate }}

- +
diff --git a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.scss b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.scss similarity index 100% rename from src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.scss rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.scss diff --git a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.spec.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.spec.ts similarity index 92% rename from src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.spec.ts rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.spec.ts index 9edbb8481..bd54fa2f2 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.spec.ts @@ -13,10 +13,10 @@ import { MOCK_LICENSE } from '@shared/mocks'; import { LicenseModel } from '@shared/models'; import { CustomConfirmationService, ToastService } from '@shared/services'; -import { ContributorsComponent } from './contributors/contributors.component'; import { PreprintsAffiliatedInstitutionsComponent } from './preprints-affiliated-institutions/preprints-affiliated-institutions.component'; +import { PreprintsContributorsComponent } from './preprints-contributors/preprints-contributors.component'; import { PreprintsSubjectsComponent } from './preprints-subjects/preprints-subjects.component'; -import { MetadataStepComponent } from './metadata-step.component'; +import { PreprintsMetadataStepComponent } from './preprints-metadata-step.component'; import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; @@ -25,9 +25,9 @@ import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom- import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; -describe('MetadataStepComponent', () => { - let component: MetadataStepComponent; - let fixture: ComponentFixture; +describe('PreprintsMetadataStepComponent', () => { + let component: PreprintsMetadataStepComponent; + let fixture: ComponentFixture; let toastServiceMock: ReturnType; let customConfirmationServiceMock: ReturnType; @@ -43,10 +43,10 @@ describe('MetadataStepComponent', () => { await TestBed.configureTestingModule({ imports: [ - MetadataStepComponent, + PreprintsMetadataStepComponent, OSFTestingModule, ...MockComponents( - ContributorsComponent, + PreprintsContributorsComponent, PreprintsAffiliatedInstitutionsComponent, PreprintsSubjectsComponent, IconComponent, @@ -77,7 +77,7 @@ describe('MetadataStepComponent', () => { ], }).compileComponents(); - fixture = TestBed.createComponent(MetadataStepComponent); + fixture = TestBed.createComponent(PreprintsMetadataStepComponent); component = fixture.componentInstance; fixture.componentRef.setInput('provider', mockProvider); fixture.detectChanges(); diff --git a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.ts similarity index 88% rename from src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.ts rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.ts index f2a60abe4..786b4aaa0 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.ts @@ -21,20 +21,20 @@ import { SaveLicense, UpdatePreprint, } from '@osf/features/preprints/store/preprint-stepper'; +import { IconComponent, LicenseComponent, TagsInputComponent, TextInputComponent } from '@osf/shared/components'; +import { INPUT_VALIDATION_MESSAGES } from '@osf/shared/constants'; import { CustomValidators, findChangedFields } from '@osf/shared/helpers'; -import { IconComponent, LicenseComponent, TagsInputComponent, TextInputComponent } from '@shared/components'; -import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; -import { LicenseModel, LicenseOptions } from '@shared/models'; -import { CustomConfirmationService, ToastService } from '@shared/services'; +import { LicenseModel, LicenseOptions } from '@osf/shared/models'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; -import { ContributorsComponent } from './contributors/contributors.component'; import { PreprintsAffiliatedInstitutionsComponent } from './preprints-affiliated-institutions/preprints-affiliated-institutions.component'; +import { PreprintsContributorsComponent } from './preprints-contributors/preprints-contributors.component'; import { PreprintsSubjectsComponent } from './preprints-subjects/preprints-subjects.component'; @Component({ - selector: 'osf-preprint-metadata', + selector: 'osf-preprints-metadata', imports: [ - ContributorsComponent, + PreprintsContributorsComponent, Button, Card, ReactiveFormsModule, @@ -50,11 +50,11 @@ import { PreprintsSubjectsComponent } from './preprints-subjects/preprints-subje PreprintsSubjectsComponent, PreprintsAffiliatedInstitutionsComponent, ], - templateUrl: './metadata-step.component.html', - styleUrl: './metadata-step.component.scss', + templateUrl: './preprints-metadata-step.component.html', + styleUrl: './preprints-metadata-step.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MetadataStepComponent implements OnInit { +export class PreprintsMetadataStepComponent implements OnInit { private customConfirmationService = inject(CustomConfirmationService); private toastService = inject(ToastService); private actions = createDispatchMap({ diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.html b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.html similarity index 100% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.html rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.html diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.scss b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.scss similarity index 100% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.scss rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.scss diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.spec.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.spec.ts similarity index 92% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.spec.ts rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.spec.ts index 330e6fda9..c8865104d 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.spec.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.spec.ts @@ -7,8 +7,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl } from '@angular/forms'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; -import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; -import { SubjectsSelectors } from '@shared/stores'; +import { MOCK_STORE, TranslateServiceMock } from '@osf/shared/mocks'; +import { SubjectsSelectors } from '@osf/shared/stores'; import { PreprintsSubjectsComponent } from './preprints-subjects.component'; diff --git a/src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.ts similarity index 97% rename from src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.ts rename to src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.ts index a88ca063c..b7d18cdbe 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/preprints-subjects/preprints-subjects.component.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-subjects/preprints-subjects.component.ts @@ -10,6 +10,7 @@ import { FormControl } from '@angular/forms'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; import { SubjectsComponent } from '@osf/shared/components'; +import { INPUT_VALIDATION_MESSAGES } from '@osf/shared/constants'; import { ResourceType } from '@osf/shared/enums'; import { SubjectModel } from '@osf/shared/models'; import { @@ -19,7 +20,6 @@ import { SubjectsSelectors, UpdateResourceSubjects, } from '@osf/shared/stores'; -import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; @Component({ selector: 'osf-preprints-subjects', diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html index 50aa02379..3bc727057 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html @@ -6,10 +6,12 @@

{{ 'preprints.selectService.sectionTitle' | translate }}

{{ 'preprints.selectService.description' | translate }} - + + {{ 'preprints.selectService.learnMore' | translate }}

+
@if (areProvidersLoading()) { @for (_ of skeletonArray; track $index) { @@ -51,12 +53,14 @@

{{ provider.name }}

/>
+
} } +
/> } @case (SubmitStepsEnum.Metadata) { - /> } @case (PreprintSteps.Metadata) { - {{ 'navigation.contributors' | translate } - this.actions.updateContributor(this.resourceId(), this.resourceType(), payload) - ); - - forkJoin(updateRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); - }); + this.actions + .bulkUpdateContributors(this.resourceId(), this.resourceType(), updatedContributors) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage') + ); } openAddContributorDialog() { @@ -216,13 +217,12 @@ export class ContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => - this.actions.addContributor(this.resourceId(), this.resourceType(), payload) - ); - - forkJoin(addRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); - }); + this.actions + .bulkAddContributors(this.resourceId(), this.resourceType(), res.data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage') + ); } }); } @@ -260,12 +260,11 @@ export class ContributorsComponent implements OnInit { this.actions .deleteContributor(this.resourceId(), this.resourceType(), contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe({ - next: () => - this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { - name: contributor.fullName, - }), - }); + .subscribe(() => + this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { + name: contributor.fullName, + }) + ); }, }); } diff --git a/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.html b/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.html index 7c626c448..a9a42db3f 100644 --- a/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.html +++ b/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.html @@ -3,7 +3,7 @@

{{ 'project.overview.metadata.contributors' | translate }}

- this.actions.updateContributor(this.draftId(), ResourceType.DraftRegistration, payload) - ); - - forkJoin(updateRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); - }); + this.actions + .bulkUpdateContributors(this.draftId(), ResourceType.DraftRegistration, updatedContributors) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage') + ); } openAddContributorDialog() { @@ -139,13 +140,12 @@ export class RegistriesContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => - this.actions.addContributor(this.draftId(), ResourceType.DraftRegistration, payload) - ); - - forkJoin(addRequests).subscribe(() => { - this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); - }); + this.actions + .bulkAddContributors(this.draftId(), ResourceType.DraftRegistration, res.data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage') + ); } }); } diff --git a/src/app/features/settings/profile-settings/components/education/education.component.ts b/src/app/features/settings/profile-settings/components/education/education.component.ts index 4ffe3bae2..af16a56d2 100644 --- a/src/app/features/settings/profile-settings/components/education/education.component.ts +++ b/src/app/features/settings/profile-settings/components/education/education.component.ts @@ -123,6 +123,10 @@ export class EducationComponent { ); } + if (formPositions.length !== education.length) { + return true; + } + return this.educations.value.some((formEducation, index) => { const initialEdu = this.educationItems()[index]; if (!initialEdu) return true; diff --git a/src/app/shared/components/contributors/contributors-table/contributors-table.component.html b/src/app/shared/components/contributors/contributors-table/contributors-table.component.html index 1cb878070..3193a56b1 100644 --- a/src/app/shared/components/contributors/contributors-table/contributors-table.component.html +++ b/src/app/shared/components/contributors/contributors-table/contributors-table.component.html @@ -9,10 +9,13 @@ [lazy]="true" [lazyLoadOnInit]="true" [customSort]="true" + [reorderableColumns]="true" + (onRowReorder)="onRowReorder()" class="view-only-table" > + {{ 'project.contributors.table.headers.name' | translate }}
@@ -55,9 +58,15 @@ - + @if (contributor.id) { - + + +
+ +
+ +

{{ contributor.fullName }} diff --git a/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts b/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts index f3977433b..03f6efa49 100644 --- a/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts +++ b/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts @@ -125,6 +125,7 @@ describe('ContributorsTableComponent', () => { fullName: 'Minimal User', givenName: 'Minimal User', familyName: 'Minimal User', + index: 0, permission: ContributorPermission.Read, education: [], employment: [], diff --git a/src/app/shared/components/contributors/contributors-table/contributors-table.component.ts b/src/app/shared/components/contributors/contributors-table/contributors-table.component.ts index 3343069ae..98a18206b 100644 --- a/src/app/shared/components/contributors/contributors-table/contributors-table.component.ts +++ b/src/app/shared/components/contributors/contributors-table/contributors-table.component.ts @@ -6,7 +6,7 @@ import { Skeleton } from 'primeng/skeleton'; import { TableModule } from 'primeng/table'; import { Tooltip } from 'primeng/tooltip'; -import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, input, model, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { EducationHistoryDialogComponent } from '@osf/shared/components/education-history-dialog/education-history-dialog.component'; @@ -17,15 +17,27 @@ import { ContributorPermission, ResourceType } from '@osf/shared/enums'; import { ContributorModel, SelectOption, TableParameters } from '@osf/shared/models'; import { CustomDialogService } from '@osf/shared/services'; +import { IconComponent } from '../../icon/icon.component'; + @Component({ selector: 'osf-contributors-table', - imports: [TranslatePipe, FormsModule, TableModule, Tooltip, Checkbox, Skeleton, Button, SelectComponent], + imports: [ + TranslatePipe, + FormsModule, + TableModule, + Tooltip, + Checkbox, + Skeleton, + Button, + SelectComponent, + IconComponent, + ], templateUrl: './contributors-table.component.html', styleUrl: './contributors-table.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ContributorsTableComponent { - contributors = input([]); + contributors = model([]); isLoading = input(false); showCurator = input(false); showEducation = input(true); @@ -85,4 +97,9 @@ export class ContributorsTableComponent { data: contributor.employment, }); } + + onRowReorder() { + const reorderedContributors = this.contributors().map((item, i) => ({ ...item, index: i })); + this.contributors.set(reorderedContributors); + } } diff --git a/src/app/shared/mappers/contributors/contributors.mapper.ts b/src/app/shared/mappers/contributors/contributors.mapper.ts index fcd1e7e33..ff33934a2 100644 --- a/src/app/shared/mappers/contributors/contributors.mapper.ts +++ b/src/app/shared/mappers/contributors/contributors.mapper.ts @@ -81,6 +81,7 @@ export class ContributorsMapper { attributes: { bibliographic: model.isBibliographic, permission: model.permission, + index: model.index, id: model.id, }, relationships: { diff --git a/src/app/shared/models/contributors/contributor-add.model.ts b/src/app/shared/models/contributors/contributor-add.model.ts index e7988c070..31bc3bf35 100644 --- a/src/app/shared/models/contributors/contributor-add.model.ts +++ b/src/app/shared/models/contributors/contributor-add.model.ts @@ -4,4 +4,5 @@ export interface ContributorAddModel { permission: string; fullName?: string; email?: string; + index?: number; } diff --git a/src/app/shared/models/contributors/contributor-response-json-api.model.ts b/src/app/shared/models/contributors/contributor-response-json-api.model.ts index c5d71471a..9d619433c 100644 --- a/src/app/shared/models/contributors/contributor-response-json-api.model.ts +++ b/src/app/shared/models/contributors/contributor-response-json-api.model.ts @@ -35,6 +35,7 @@ export interface ContributorAddRequestModel { bibliographic: boolean; permission: string; id?: string; + index?: number; full_name?: string; email?: string; }; diff --git a/src/app/shared/services/contributors.service.ts b/src/app/shared/services/contributors.service.ts index e350ed35a..fdeb8a157 100644 --- a/src/app/shared/services/contributors.service.ts +++ b/src/app/shared/services/contributors.service.ts @@ -1,4 +1,4 @@ -import { map, Observable } from 'rxjs'; +import { forkJoin, map, Observable, of } from 'rxjs'; import { inject, Injectable } from '@angular/core'; @@ -63,19 +63,20 @@ export class ContributorsService { .pipe(map((response) => ContributorsMapper.getPaginatedUsers(response))); } - addContributor( + bulkUpdateContributors( resourceType: ResourceType, resourceId: string, - data: ContributorAddModel - ): Observable { - const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/`; - const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; + contributors: ContributorModel[] + ): Observable { + if (contributors.length === 0) { + return of([]); + } - const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; + const updateRequests = contributors.map((contributor) => + this.updateContributor(resourceType, resourceId, contributor) + ); - return this.jsonApiService - .post(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.getContributor(contributor.data))); + return forkJoin(updateRequests); } updateContributor( @@ -92,6 +93,35 @@ export class ContributorsService { .pipe(map((contributor) => ContributorsMapper.getContributor(contributor))); } + bulkAddContributors( + resourceType: ResourceType, + resourceId: string, + contributors: ContributorAddModel[] + ): Observable { + if (contributors.length === 0) { + return of([]); + } + + const addRequests = contributors.map((contributor) => this.addContributor(resourceType, resourceId, contributor)); + + return forkJoin(addRequests); + } + + addContributor( + resourceType: ResourceType, + resourceId: string, + data: ContributorAddModel + ): Observable { + const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/`; + const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; + + const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; + + return this.jsonApiService + .post(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.getContributor(contributor.data))); + } + deleteContributor(resourceType: ResourceType, resourceId: string, userId: string): Observable { const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${userId}/`; diff --git a/src/app/shared/stores/contributors/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts index d93e9c89b..8633da276 100644 --- a/src/app/shared/stores/contributors/contributors.actions.ts +++ b/src/app/shared/stores/contributors/contributors.actions.ts @@ -38,13 +38,23 @@ export class AddContributor { ) {} } -export class UpdateContributor { - static readonly type = '[Contributors] Update Contributor'; +export class BulkUpdateContributors { + static readonly type = '[Contributors] Bulk Update Contributors'; constructor( public resourceId: string | undefined | null, public resourceType: ResourceType | undefined, - public contributor: ContributorModel + public contributors: ContributorModel[] + ) {} +} + +export class BulkAddContributors { + static readonly type = '[Contributors] Bulk Add Contributors'; + + constructor( + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined, + public contributors: ContributorAddModel[] ) {} } diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts index bfd7558d2..fbef29ec8 100644 --- a/src/app/shared/stores/contributors/contributors.state.ts +++ b/src/app/shared/stores/contributors/contributors.state.ts @@ -9,13 +9,14 @@ import { ContributorsService } from '@osf/shared/services'; import { AddContributor, + BulkAddContributors, + BulkUpdateContributors, ClearUsers, DeleteContributor, GetAllContributors, ResetContributorsState, SearchUsers, UpdateBibliographyFilter, - UpdateContributor, UpdateContributorsSearchValue, UpdatePermissionFilter, } from './contributors.actions'; @@ -33,14 +34,14 @@ export class ContributorsState { getAllContributors(ctx: StateContext, action: GetAllContributors) { const state = ctx.getState(); - ctx.patchState({ - contributorsList: { ...state.contributorsList, data: [], isLoading: true, error: null }, - }); - if (!action.resourceId || !action.resourceType) { return; } + ctx.patchState({ + contributorsList: { ...state.contributorsList, data: [], isLoading: true, error: null }, + }); + return this.contributorsService.getAllContributors(action.resourceType, action.resourceId).pipe( tap((contributors) => { ctx.patchState({ @@ -59,14 +60,14 @@ export class ContributorsState { addContributor(ctx: StateContext, action: AddContributor) { const state = ctx.getState(); - ctx.patchState({ - contributorsList: { ...state.contributorsList, isLoading: true, error: null }, - }); - if (!action.resourceId || !action.resourceType) { return; } + ctx.patchState({ + contributorsList: { ...state.contributorsList, isLoading: true, error: null }, + }); + return this.contributorsService.addContributor(action.resourceType, action.resourceId, action.contributor).pipe( tap((contributor) => { const currentState = ctx.getState(); @@ -83,48 +84,62 @@ export class ContributorsState { ); } - @Action(UpdateContributor) - updateContributor(ctx: StateContext, action: UpdateContributor) { + @Action(BulkUpdateContributors) + bulkUpdateContributors(ctx: StateContext, action: BulkUpdateContributors) { const state = ctx.getState(); + if (!action.resourceId || !action.resourceType || !action.contributors.length) { + return; + } + ctx.patchState({ contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); - if (!action.resourceId || !action.resourceType) { + return this.contributorsService + .bulkUpdateContributors(action.resourceType, action.resourceId, action.contributors) + .pipe( + tap(() => { + ctx.dispatch(new GetAllContributors(action.resourceId, action.resourceType)); + }), + catchError((error) => handleSectionError(ctx, 'contributorsList', error)) + ); + } + + @Action(BulkAddContributors) + bulkAddContributors(ctx: StateContext, action: BulkAddContributors) { + const state = ctx.getState(); + + if (!action.resourceId || !action.resourceType || !action.contributors.length) { return; } - return this.contributorsService.updateContributor(action.resourceType, action.resourceId, action.contributor).pipe( - tap((updatedContributor) => { - const currentState = ctx.getState(); + ctx.patchState({ + contributorsList: { ...state.contributorsList, isLoading: true, error: null }, + }); - ctx.patchState({ - contributorsList: { - ...currentState.contributorsList, - data: currentState.contributorsList.data.map((contributor) => - contributor.id === updatedContributor.id ? updatedContributor : contributor - ), - isLoading: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'contributorsList', error)) - ); + return this.contributorsService + .bulkAddContributors(action.resourceType, action.resourceId, action.contributors) + .pipe( + tap(() => { + ctx.dispatch(new GetAllContributors(action.resourceId, action.resourceType)); + }), + catchError((error) => handleSectionError(ctx, 'contributorsList', error)) + ); } @Action(DeleteContributor) deleteContributor(ctx: StateContext, action: DeleteContributor) { const state = ctx.getState(); - ctx.patchState({ - contributorsList: { ...state.contributorsList, isLoading: true, error: null }, - }); - if (!action.resourceId || !action.resourceType) { return; } + ctx.patchState({ + contributorsList: { ...state.contributorsList, isLoading: true, error: null }, + }); + return this.contributorsService .deleteContributor(action.resourceType, action.resourceId, action.contributorId) .pipe(