diff --git a/jest.config.js b/jest.config.js index ecdfa34e0..f6ee835f3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -50,10 +50,10 @@ module.exports = { extensionsToTreatAsEsm: ['.ts'], coverageThreshold: { global: { - branches: 24.1, - functions: 28.73, - lines: 56.52, - statements: 56.82, + branches: 29.01, + functions: 32.75, + lines: 60.28, + statements: 60.77, }, }, watchPathIgnorePatterns: [ diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 409a3d258..b41f1c6a5 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -6,7 +6,7 @@ import { FilesState } from '@osf/features/files/store'; import { MetadataState } from '@osf/features/metadata/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { RegistrationsState } from '@osf/features/project/registrations/store'; -import { AddonsState, CurrentResourceState, WikiState } from '@osf/shared/stores'; +import { AddonsState, ContributorsState, CurrentResourceState, WikiState } from '@osf/shared/stores'; import { BannersState } from '@osf/shared/stores/banners'; import { GlobalSearchState } from '@shared/stores/global-search'; import { InstitutionsState } from '@shared/stores/institutions'; @@ -36,4 +36,5 @@ export const STATES = [ GlobalSearchState, BannersState, LinkedProjectsState, + ContributorsState, ]; diff --git a/src/app/features/collections/collections.routes.ts b/src/app/features/collections/collections.routes.ts index cb95bfe26..851ebacb3 100644 --- a/src/app/features/collections/collections.routes.ts +++ b/src/app/features/collections/collections.routes.ts @@ -6,14 +6,7 @@ import { authGuard } from '@osf/core/guards'; 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, - CitationsState, - ContributorsState, - NodeLinksState, - ProjectsState, - SubjectsState, -} from '@shared/stores'; +import { BookmarksState, CitationsState, NodeLinksState, ProjectsState, SubjectsState } from '@shared/stores'; import { CollectionsState } from '@shared/stores/collections'; export const collectionsRoutes: Routes = [ @@ -47,7 +40,7 @@ export const collectionsRoutes: Routes = [ import('@osf/features/collections/components/add-to-collection/add-to-collection.component').then( (mod) => mod.AddToCollectionComponent ), - providers: [provideStates([ProjectsState, CollectionsState, AddToCollectionState, ContributorsState])], + providers: [provideStates([ProjectsState, CollectionsState, AddToCollectionState])], canActivate: [authGuard], canDeactivate: [ConfirmLeavingGuard], }, diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.html b/src/app/features/collections/components/add-to-collection/add-to-collection.component.html index 204620fa5..e60b9c227 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.html +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.html @@ -32,6 +32,7 @@

{{ collectionProvider()? /> {{ 'collections.addToCollection.projectContributors' | translate }}

[(contributors)]="projectContributors" [tableParams]="tableParams()" [isLoading]="isContributorsLoading()" + [isLoadingMore]="isLoadingMore()" (remove)="handleRemoveContributor($event)" + (loadMore)="loadMoreContributors()" >
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 f6518e2d4..99581d068 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 @@ -40,6 +40,7 @@ import { BulkUpdateContributors, ContributorsSelectors, DeleteContributor, + LoadMoreContributors, ProjectsSelectors, } from '@osf/shared/stores'; @@ -61,20 +62,26 @@ export class ProjectContributorsStepComponent { readonly contributorsTotalCount = select(ContributorsSelectors.getContributorsTotalCount); readonly selectedProject = select(ProjectsSelectors.getSelectedProject); readonly currentUser = select(UserSelectors.getCurrentUser); + isLoadingMore = select(ContributorsSelectors.isContributorsLoadingMore); private initialContributors = select(ContributorsSelectors.getContributors); readonly projectContributors = signal([]); + pageSize = select(ContributorsSelectors.getContributorsPageSize); readonly tableParams = computed(() => ({ ...DEFAULT_TABLE_PARAMS, totalRecords: this.contributorsTotalCount(), - paginator: this.contributorsTotalCount() > DEFAULT_TABLE_PARAMS.rows, + paginator: false, + scrollable: true, + firstRowIndex: 0, + rows: this.pageSize(), })); stepperActiveValue = input.required(); targetStepValue = input.required(); isDisabled = input.required(); isProjectMetadataSaved = input(false); + projectId = input(); stepChange = output(); contributorsSaved = output(); @@ -84,6 +91,7 @@ export class ProjectContributorsStepComponent { bulkAddContributors: BulkAddContributors, bulkUpdateContributors: BulkUpdateContributors, deleteContributor: DeleteContributor, + loadMoreContributors: LoadMoreContributors, }); constructor() { @@ -150,14 +158,15 @@ export class ProjectContributorsStepComponent { this.stepChange.emit(this.targetStepValue()); } - private openAddContributorDialog() { - const addedContributorIds = this.projectContributors().map((x) => x.userId); + loadMoreContributors(): void { + this.actions.loadMoreContributors(this.projectId(), ResourceType.Project); + } + private openAddContributorDialog() { this.customDialogService .open(AddContributorDialogComponent, { header: 'project.contributors.addDialog.addRegisteredContributor', width: '448px', - data: addedContributorIds, }) .onClose.pipe( filter((res: ContributorDialogAddModel) => !!res), diff --git a/src/app/features/contributors/contributors.component.html b/src/app/features/contributors/contributors.component.html index dd3d74d45..3a5785bee 100644 --- a/src/app/features/contributors/contributors.component.html +++ b/src/app/features/contributors/contributors.component.html @@ -63,6 +63,7 @@

{{ 'navigation.contributors' | translate } class="w-full" [(contributors)]="contributors" [isLoading]="isContributorsLoading()" + [isLoadingMore]="isLoadingMore()" [tableParams]="tableParams()" [hasAdminAccess]="hasAdminAccess()" [currentUserId]="currentUser()?.id" @@ -70,7 +71,7 @@

{{ 'navigation.contributors' | translate } [showInfo]="true" [resourceType]="resourceType()" (remove)="removeContributor($event)" - (pageChanged)="pageChanged($event)" + (loadMore)="loadMoreContributors()" > @if (hasChanges) { diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts index 5c6370d7b..226957548 100644 --- a/src/app/features/contributors/contributors.component.ts +++ b/src/app/features/contributors/contributors.component.ts @@ -4,7 +4,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Select } from 'primeng/select'; -import { TableModule, TablePageEvent } from 'primeng/table'; +import { TableModule } from 'primeng/table'; import { debounceTime, distinctUntilChanged, filter, map, of, switchMap } from 'rxjs'; @@ -15,6 +15,7 @@ import { DestroyRef, effect, inject, + OnDestroy, OnInit, signal, } from '@angular/core'; @@ -61,7 +62,9 @@ import { GetRequestAccessContributors, GetResourceDetails, GetResourceWithChildren, + LoadMoreContributors, RejectRequestAccess, + ResetContributorsState, UpdateBibliographyFilter, UpdateContributorsSearchValue, UpdatePermissionFilter, @@ -88,7 +91,7 @@ import { ResourceInfoModel } from './models'; styleUrl: './contributors.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ContributorsComponent implements OnInit { +export class ContributorsComponent implements OnInit, OnDestroy { searchControl = new FormControl(''); readonly destroyRef = inject(DestroyRef); @@ -124,14 +127,15 @@ export class ContributorsComponent implements OnInit { readonly hasAdminAccess = select(CurrentResourceSelectors.hasResourceAdminAccess); readonly resourceAccessRequestEnabled = select(CurrentResourceSelectors.resourceAccessRequestEnabled); readonly currentUser = select(UserSelectors.getCurrentUser); - page = select(ContributorsSelectors.getContributorsPageNumber); pageSize = select(ContributorsSelectors.getContributorsPageSize); + isLoadingMore = select(ContributorsSelectors.isContributorsLoadingMore); readonly tableParams = computed(() => ({ ...DEFAULT_TABLE_PARAMS, totalRecords: this.contributorsTotalCount(), - paginator: this.contributorsTotalCount() > DEFAULT_TABLE_PARAMS.rows, - firstRowIndex: (this.page() - 1) * this.pageSize(), + paginator: false, + scrollable: true, + firstRowIndex: 0, rows: this.pageSize(), })); @@ -154,6 +158,7 @@ export class ContributorsComponent implements OnInit { getViewOnlyLinks: FetchViewOnlyLinks, getResourceDetails: GetResourceDetails, getContributors: GetAllContributors, + loadMoreContributors: LoadMoreContributors, updateSearchValue: UpdateContributorsSearchValue, updatePermissionFilter: UpdatePermissionFilter, updateBibliographyFilter: UpdateBibliographyFilter, @@ -168,6 +173,7 @@ export class ContributorsComponent implements OnInit { acceptRequestAccess: AcceptRequestAccess, rejectRequestAccess: RejectRequestAccess, getResourceWithChildren: GetResourceWithChildren, + resetContributorsState: ResetContributorsState, }); get hasChanges(): boolean { @@ -189,6 +195,10 @@ export class ContributorsComponent implements OnInit { this.setSearchSubscription(); } + ngOnDestroy(): void { + this.actions.resetContributorsState(); + } + private setSearchSubscription() { this.searchControl.valueChanges .pipe(debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) @@ -247,7 +257,6 @@ export class ContributorsComponent implements OnInit { } openAddContributorDialog() { - const addedContributorIds = this.initialContributors().map((x) => x.userId); const resourceDetails = this.resourceDetails(); const resourceId = this.resourceId(); const rootParentId = resourceDetails.rootParentId ?? resourceId; @@ -267,7 +276,6 @@ export class ContributorsComponent implements OnInit { header: 'project.contributors.addDialog.addRegisteredContributor', width: '448px', data: { - addedContributorIds, components, resourceName: resourceDetails.title, parentResourceName: resourceDetails.parent?.title, @@ -401,11 +409,8 @@ export class ContributorsComponent implements OnInit { }); } - pageChanged(event: TablePageEvent) { - const page = Math.floor(event.first / event.rows) + 1; - const pageSize = event.rows; - - this.actions.getContributors(this.resourceId(), this.resourceType(), page, pageSize); + loadMoreContributors(): void { + this.actions.loadMoreContributors(this.resourceId(), this.resourceType()); } createViewLink() { diff --git a/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.html b/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.html index e7956426c..3ec6df984 100644 --- a/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.html +++ b/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.html @@ -12,9 +12,14 @@

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

}

- @if (contributors()) { -
- + @if (contributors().length) { +
+
} diff --git a/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.ts b/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.ts index 461edea00..360c4b225 100644 --- a/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.ts +++ b/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.ts @@ -15,7 +15,11 @@ import { ContributorModel } from '@osf/shared/models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class MetadataContributorsComponent { - openEditContributorDialog = output(); contributors = input([]); + isLoading = input(false); + hasMoreContributors = input(false); readonly = input(false); + + openEditContributorDialog = output(); + loadMoreContributors = output(); } 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 b6ee30cba..4ff339220 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 @@ -19,12 +19,13 @@ [(contributors)]="contributors" [tableParams]="tableParams()" [isLoading]="isLoading()" + [isLoadingMore]="isLoadingMore()" [showEmployment]="false" [showEducation]="false" [hasAdminAccess]="hasAdminAccess()" [currentUserId]="currentUser()?.id" (remove)="removeContributor($event)" - (pageChanged)="pageChanged($event)" + (loadMore)="loadMoreContributors()" > @if (hasChanges) { diff --git a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts index dd5693767..d3f9fd331 100644 --- a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts +++ b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts @@ -4,7 +4,6 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { TablePageEvent } from 'primeng/table'; import { filter } from 'rxjs'; @@ -41,6 +40,7 @@ import { ContributorsSelectors, DeleteContributor, GetAllContributors, + LoadMoreContributors, UpdateBibliographyFilter, UpdateContributorsSearchValue, UpdatePermissionFilter, @@ -70,16 +70,18 @@ export class ContributorsDialogComponent implements OnInit { contributorsTotalCount = select(ContributorsSelectors.getContributorsTotalCount); hasAdminAccess = select(MetadataSelectors.hasAdminAccess); contributors = signal([]); - page = select(ContributorsSelectors.getContributorsPageNumber); + isLoadingMore = select(ContributorsSelectors.isContributorsLoadingMore); pageSize = select(ContributorsSelectors.getContributorsPageSize); + changesMade = signal(false); currentUser = select(UserSelectors.getCurrentUser); readonly tableParams = computed(() => ({ ...DEFAULT_TABLE_PARAMS, totalRecords: this.contributorsTotalCount(), - paginator: this.contributorsTotalCount() > DEFAULT_TABLE_PARAMS.rows, - firstRowIndex: (this.page() - 1) * this.pageSize(), + paginator: false, + scrollable: true, + firstRowIndex: 0, rows: this.pageSize(), })); @@ -92,6 +94,7 @@ export class ContributorsDialogComponent implements OnInit { addContributor: AddContributor, bulkAddContributors: BulkAddContributors, bulkUpdateContributors: BulkUpdateContributors, + loadMoreContributors: LoadMoreContributors, }); private readonly resourceType: ResourceType; @@ -117,6 +120,7 @@ export class ContributorsDialogComponent implements OnInit { } ngOnInit(): void { + this.actions.getContributors(this.resourceId, this.resourceType); this.setSearchSubscription(); } @@ -127,13 +131,10 @@ export class ContributorsDialogComponent implements OnInit { } openAddContributorDialog(): void { - const addedContributorIds = this.initialContributors().map((x) => x.userId); - this.customDialogService .open(AddContributorDialogComponent, { header: 'project.contributors.addDialog.addRegisteredContributor', width: '448px', - data: addedContributorIds, }) .onClose.pipe( filter((res: ContributorDialogAddModel) => !!res), @@ -147,9 +148,10 @@ export class ContributorsDialogComponent implements OnInit { this.actions .bulkAddContributors(this.resourceId, this.resourceType, res.data) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => - this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage') - ); + .subscribe(() => { + this.changesMade.set(true); + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); + }); } } }); @@ -172,7 +174,10 @@ export class ContributorsDialogComponent implements OnInit { const params = { name: res.data[0].fullName }; this.actions.addContributor(this.resourceId, this.resourceType, res.data[0]).subscribe({ - next: () => this.toastService.showSuccess('project.contributors.toastMessages.addSuccessMessage', params), + next: () => { + this.changesMade.set(true); + this.toastService.showSuccess('project.contributors.toastMessages.addSuccessMessage', params); + }, }); } }); @@ -192,12 +197,13 @@ export class ContributorsDialogComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { + this.changesMade.set(true); this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { name: contributor.fullName, }); if (isDeletingSelf) { - this.dialogRef.close(); + this.dialogRef.close(this.changesMade()); this.router.navigate(['/']); } }, @@ -206,11 +212,8 @@ export class ContributorsDialogComponent implements OnInit { }); } - pageChanged(event: TablePageEvent) { - const page = Math.floor(event.first / event.rows) + 1; - const pageSize = event.rows; - - this.actions.getContributors(this.resourceId, this.resourceType, page, pageSize); + loadMoreContributors(): void { + this.actions.loadMoreContributors(this.resourceId, this.resourceType); } cancel() { @@ -218,7 +221,7 @@ export class ContributorsDialogComponent implements OnInit { } onClose(): void { - this.dialogRef.close(); + this.dialogRef.close(this.changesMade()); } onSave(): void { @@ -227,8 +230,9 @@ export class ContributorsDialogComponent implements OnInit { this.actions .bulkUpdateContributors(this.resourceId, this.resourceType, updatedContributors) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => - this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage') - ); + .subscribe(() => { + this.changesMade.set(true); + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); + }); } } diff --git a/src/app/features/metadata/metadata.component.html b/src/app/features/metadata/metadata.component.html index a5adc3975..fe576760d 100644 --- a/src/app/features/metadata/metadata.component.html +++ b/src/app/features/metadata/metadata.component.html @@ -38,7 +38,10 @@ this.isMetadataLoading() || - this.isContributorsLoading() || this.areInstitutionsLoading() || this.isSubmitting() || this.areResourceInstitutionsSubmitting() @@ -320,23 +322,24 @@ export class MetadataComponent implements OnInit { this.actions.updateMetadata(this.resourceId, this.resourceType(), { tags }); } + handleLoadMoreContributors(): void { + this.actions.loadMoreBibliographicContributors(this.resourceId, this.resourceType()); + } + openEditContributorDialog(): void { this.customDialogService .open(ContributorsDialogComponent, { header: 'project.metadata.contributors.editContributors', - width: '600px', + width: '800px', data: { resourceId: this.resourceId, resourceType: this.resourceType(), }, }) - .onClose.subscribe((result) => { - if (result) { - this.actions.getResourceMetadata(this.resourceId, this.resourceType()); - this.toastService.showSuccess('project.metadata.contributors.updateSucceed'); + .onClose.subscribe((changesMade) => { + if (changesMade) { + this.actions.getContributors(this.resourceId, this.resourceType()); } - - this.actions.updateContributorsSearchValue(null); }); } diff --git a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html index ae7decd48..8de29f158 100644 --- a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html +++ b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html @@ -39,7 +39,7 @@ -
+

{{ 'common.labels.contributors' | translate }}:

@if (submission().contributorsLoading) { diff --git a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html index c5c929c7e..cca778583 100644 --- a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html +++ b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html @@ -53,7 +53,7 @@

{{ 'preprints.preprintStepper.review.sections.metadata.subjects' | translate } @if (areSelectedSubjectsLoading()) { - + }

@@ -83,8 +83,8 @@

{{ 'preprints.preprintStepper.review.sections.metadata.tags' | translate }}<
@for (i of skeletonData; track $index) {
- - + +
}
diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html index 1864e4d31..a20d43ff2 100644 --- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html +++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html @@ -7,11 +7,12 @@

{{ 'preprints.preprintStepper.review.sections.metadata.authors' | translate }}

- - - @if (areContributorsLoading()) { - - } +
@@ -118,8 +119,8 @@

{{ 'preprints.preprintStepper.review.sections.authorAssertions.conflictOfInt
@for (i of skeletonData; track $index) {
- - + +
}
diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts index e1e4de9c4..c5e129de1 100644 --- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.spec.ts @@ -47,9 +47,7 @@ describe('GeneralInformationComponent', () => { ), ], providers: [ - MockProvider(ENVIRONMENT, { - webUrl: mockWebUrl, - }), + MockProvider(ENVIRONMENT, { webUrl: mockWebUrl }), provideMockStore({ signals: [ { @@ -61,11 +59,15 @@ describe('GeneralInformationComponent', () => { value: false, }, { - selector: ContributorsSelectors.getContributors, + selector: ContributorsSelectors.getBibliographicContributors, value: mockContributors, }, { - selector: ContributorsSelectors.isContributorsLoading, + selector: ContributorsSelectors.isBibliographicContributorsLoading, + value: false, + }, + { + selector: ContributorsSelectors.hasMoreBibliographicContributors, value: false, }, { @@ -83,26 +85,16 @@ describe('GeneralInformationComponent', () => { fixture.componentRef.setInput('preprintProvider', mockPreprintProvider); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should return preprint from store', () => { const preprint = component.preprint(); expect(preprint).toBe(mockPreprint); }); it('should return contributors from store', () => { - const contributors = component.contributors(); + const contributors = component.bibliographicContributors(); expect(contributors).toBe(mockContributors); }); - it('should filter bibliographic contributors', () => { - const bibliographicContributors = component.bibliographicContributors(); - expect(bibliographicContributors).toHaveLength(1); - expect(bibliographicContributors.every((contributor) => contributor.isBibliographic)).toBe(true); - }); - it('should return affiliated institutions from store', () => { const institutions = component.affiliatedInstitutions(); expect(institutions).toBe(mockInstitutions); diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts index c3cbfac6b..b2af64189 100644 --- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts +++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts @@ -22,8 +22,9 @@ import { ResourceType } from '@osf/shared/enums'; import { ContributorsSelectors, FetchResourceInstitutions, - GetAllContributors, + GetBibliographicContributors, InstitutionsSelectors, + LoadMoreBibliographicContributors, ResetContributorsState, } from '@osf/shared/stores'; @@ -53,10 +54,11 @@ export class GeneralInformationComponent implements OnDestroy { readonly PreregLinkInfo = PreregLinkInfo; private actions = createDispatchMap({ - getContributors: GetAllContributors, + getBibliographicContributors: GetBibliographicContributors, resetContributorsState: ResetContributorsState, fetchPreprintById: FetchPreprintById, fetchResourceInstitutions: FetchResourceInstitutions, + loadMoreBibliographicContributors: LoadMoreBibliographicContributors, }); preprintProvider = input.required(); @@ -67,9 +69,9 @@ export class GeneralInformationComponent implements OnDestroy { affiliatedInstitutions = select(InstitutionsSelectors.getResourceInstitutions); - contributors = select(ContributorsSelectors.getContributors); - areContributorsLoading = select(ContributorsSelectors.isContributorsLoading); - bibliographicContributors = computed(() => this.contributors().filter((contributor) => contributor.isBibliographic)); + bibliographicContributors = select(ContributorsSelectors.getBibliographicContributors); + areContributorsLoading = select(ContributorsSelectors.isBibliographicContributorsLoading); + hasMoreBibliographicContributors = select(ContributorsSelectors.hasMoreBibliographicContributors); skeletonData = Array.from({ length: 5 }, () => null); @@ -80,7 +82,7 @@ export class GeneralInformationComponent implements OnDestroy { const preprint = this.preprint(); if (!preprint) return; - this.actions.getContributors(this.preprint()!.id, ResourceType.Preprint); + this.actions.getBibliographicContributors(this.preprint()!.id, ResourceType.Preprint); this.actions.fetchResourceInstitutions(this.preprint()!.id, ResourceType.Preprint); }); } @@ -88,4 +90,8 @@ export class GeneralInformationComponent implements OnDestroy { ngOnDestroy(): void { this.actions.resetContributorsState(); } + + handleLoadMoreContributors(): void { + this.actions.loadMoreBibliographicContributors(this.preprint()?.id, ResourceType.Preprint); + } } diff --git a/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.scss b/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.scss index c205f2091..7aa21b9e3 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.scss +++ b/src/app/features/preprints/components/preprint-details/preprint-file-section/preprint-file-section.component.scss @@ -4,7 +4,7 @@ } .file-section-height { - min-height: 400px; + min-height: 600px; } .card { diff --git a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html index 901890563..633711405 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html +++ b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html @@ -13,11 +13,12 @@

{{ 'preprints.details.reasonForWithdrawal' | translate }}

{{ 'preprints.preprintStepper.review.sections.metadata.authors' | translate }}

- - - @if (areContributorsLoading()) { - - } +
diff --git a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts index ecd1053a5..be2c331e4 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.spec.ts @@ -50,11 +50,15 @@ describe('PreprintTombstoneComponent', () => { value: false, }, { - selector: ContributorsSelectors.getContributors, + selector: ContributorsSelectors.getBibliographicContributors, value: mockContributors, }, { - selector: ContributorsSelectors.isContributorsLoading, + selector: ContributorsSelectors.isBibliographicContributorsLoading, + value: false, + }, + { + selector: ContributorsSelectors.hasMoreBibliographicContributors, value: false, }, { @@ -76,16 +80,6 @@ describe('PreprintTombstoneComponent', () => { fixture.componentRef.setInput('preprintProvider', mockProvider); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should compute bibliographic contributors', () => { - const bibliographicContributors = component.bibliographicContributors(); - expect(bibliographicContributors).toHaveLength(1); - expect(bibliographicContributors[0].isBibliographic).toBe(true); - }); - it('should compute license from preprint', () => { const license = component.license(); expect(license).toBe(mockPreprint.embeddedLicense); diff --git a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts index 347a8a2ad..bb410923c 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts +++ b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts @@ -20,7 +20,8 @@ import { InterpolatePipe } from '@osf/shared/pipes'; import { ContributorsSelectors, FetchSelectedSubjects, - GetAllContributors, + GetBibliographicContributors, + LoadMoreBibliographicContributors, ResetContributorsState, SubjectsSelectors, } from '@osf/shared/stores'; @@ -53,10 +54,11 @@ export class PreprintTombstoneComponent implements OnDestroy { readonly PreregLinkInfo = PreregLinkInfo; private actions = createDispatchMap({ - getContributors: GetAllContributors, + getBibliographicContributors: GetBibliographicContributors, resetContributorsState: ResetContributorsState, fetchPreprintById: FetchPreprintById, fetchSubjects: FetchSelectedSubjects, + loadMoreBibliographicContributors: LoadMoreBibliographicContributors, }); private router = inject(Router); @@ -67,9 +69,9 @@ export class PreprintTombstoneComponent implements OnDestroy { preprint = select(PreprintSelectors.getPreprint); isPreprintLoading = select(PreprintSelectors.isPreprintLoading); - contributors = select(ContributorsSelectors.getContributors); - areContributorsLoading = select(ContributorsSelectors.isContributorsLoading); - bibliographicContributors = computed(() => this.contributors().filter((contributor) => contributor.isBibliographic)); + bibliographicContributors = select(ContributorsSelectors.getBibliographicContributors); + areContributorsLoading = select(ContributorsSelectors.isBibliographicContributorsLoading); + hasMoreBibliographicContributors = select(ContributorsSelectors.hasMoreBibliographicContributors); subjects = select(SubjectsSelectors.getSelectedSubjects); areSelectedSubjectsLoading = select(SubjectsSelectors.areSelectedSubjectsLoading); @@ -88,7 +90,7 @@ export class PreprintTombstoneComponent implements OnDestroy { const preprint = this.preprint(); if (!preprint) return; - this.actions.getContributors(this.preprint()!.id, ResourceType.Preprint); + this.actions.getBibliographicContributors(this.preprint()?.id, ResourceType.Preprint); this.actions.fetchSubjects(this.preprint()!.id, ResourceType.Preprint); }); } @@ -100,4 +102,8 @@ export class PreprintTombstoneComponent implements OnDestroy { tagClicked(tag: string) { this.router.navigate(['/search'], { queryParams: { search: tag } }); } + + loadMoreContributors(): void { + this.actions.loadMoreBibliographicContributors(this.preprint()?.id, ResourceType.Preprint); + } } diff --git a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.html b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.html index 87e081260..c5dab158f 100644 --- a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.html +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.html @@ -12,8 +12,9 @@

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

[(contributors)]="contributors" [tableParams]="tableParams()" [isLoading]="isContributorsLoading()" + [isLoadingMore]="isLoadingMore()" (remove)="removeContributor($event)" - (pageChanged)="pageChanged($event)" + (loadMore)="loadMoreContributors()" />
@if (hasChanges) { diff --git a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.ts index b2c08a55f..0331b822f 100644 --- a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-contributors/preprints-contributors.component.ts @@ -5,7 +5,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { Message } from 'primeng/message'; -import { TableModule, TablePageEvent } from 'primeng/table'; +import { TableModule } from 'primeng/table'; import { filter } from 'rxjs'; @@ -41,6 +41,7 @@ import { ContributorsSelectors, DeleteContributor, GetAllContributors, + LoadMoreContributors, } from '@osf/shared/stores'; @Component({ @@ -63,14 +64,15 @@ export class PreprintsContributorsComponent implements OnInit { contributors = signal([]); contributorsTotalCount = select(ContributorsSelectors.getContributorsTotalCount); isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); - page = select(ContributorsSelectors.getContributorsPageNumber); + isLoadingMore = select(ContributorsSelectors.isContributorsLoadingMore); pageSize = select(ContributorsSelectors.getContributorsPageSize); readonly tableParams = computed(() => ({ ...DEFAULT_TABLE_PARAMS, totalRecords: this.contributorsTotalCount(), - paginator: this.contributorsTotalCount() > DEFAULT_TABLE_PARAMS.rows, - firstRowIndex: (this.page() - 1) * this.pageSize(), + paginator: false, + scrollable: true, + firstRowIndex: 0, rows: this.pageSize(), })); @@ -80,6 +82,7 @@ export class PreprintsContributorsComponent implements OnInit { bulkUpdateContributors: BulkUpdateContributors, bulkAddContributors: BulkAddContributors, addContributor: AddContributor, + loadMoreContributors: LoadMoreContributors, }); get hasChanges(): boolean { @@ -112,13 +115,10 @@ export class PreprintsContributorsComponent implements OnInit { } openAddContributorDialog() { - const addedContributorIds = this.initialContributors().map((x) => x.userId); - this.customDialogService .open(AddContributorDialogComponent, { header: 'project.contributors.addDialog.addRegisteredContributor', width: '448px', - data: addedContributorIds, }) .onClose.pipe( filter((res: ContributorDialogAddModel) => !!res), @@ -182,10 +182,7 @@ export class PreprintsContributorsComponent implements OnInit { }); } - pageChanged(event: TablePageEvent) { - const page = Math.floor(event.first / event.rows) + 1; - const pageSize = event.rows; - - this.actions.getContributors(this.preprintId(), ResourceType.Preprint, page, pageSize); + loadMoreContributors(): void { + this.actions.loadMoreContributors(this.preprintId(), ResourceType.Preprint); } } diff --git a/src/app/features/preprints/components/stepper/review-step/review-step.component.html b/src/app/features/preprints/components/stepper/review-step/review-step.component.html index c4eb7d713..e0e092c86 100644 --- a/src/app/features/preprints/components/stepper/review-step/review-step.component.html +++ b/src/app/features/preprints/components/stepper/review-step/review-step.component.html @@ -62,7 +62,12 @@

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

{{ 'common.labels.contributors' | translate }}

- +
@if (affiliatedInstitutions().length) { diff --git a/src/app/features/preprints/components/stepper/review-step/review-step.component.spec.ts b/src/app/features/preprints/components/stepper/review-step/review-step.component.spec.ts index 16530d553..335b27a9f 100644 --- a/src/app/features/preprints/components/stepper/review-step/review-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/review-step/review-step.component.spec.ts @@ -52,7 +52,9 @@ describe('ReviewStepComponent', () => { { selector: PreprintStepperSelectors.isPreprintSubmitting, value: false }, { selector: PreprintStepperSelectors.getPreprintLicense, value: mockLicense }, { selector: PreprintStepperSelectors.getPreprintProject, value: mockPreprintProject }, - { selector: ContributorsSelectors.getContributors, value: mockContributors }, + { selector: ContributorsSelectors.getBibliographicContributors, value: mockContributors }, + { selector: ContributorsSelectors.isBibliographicContributorsLoading, value: false }, + { selector: ContributorsSelectors.hasMoreBibliographicContributors, value: false }, { selector: SubjectsSelectors.getSelectedSubjects, value: mockSubjects }, { selector: InstitutionsSelectors.getResourceInstitutions, value: mockInstitutions }, ], @@ -68,20 +70,10 @@ describe('ReviewStepComponent', () => { fixture.componentRef.setInput('provider', mockProvider); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should have required provider input', () => { expect(component.provider()).toEqual(mockProvider); }); - it('should filter bibliographic contributors', () => { - const bibliographicContributors = component.bibliographicContributors(); - expect(bibliographicContributors).toHaveLength(1); - expect(bibliographicContributors.every((c) => c.isBibliographic)).toBe(true); - }); - it('should create license options record', () => { const licenseOptionsRecord = component.licenseOptionsRecord(); expect(licenseOptionsRecord).toEqual({ copyrightHolders: 'John Doe', year: '2023' }); diff --git a/src/app/features/preprints/components/stepper/review-step/review-step.component.ts b/src/app/features/preprints/components/stepper/review-step/review-step.component.ts index 132fa9dde..7c277d1d5 100644 --- a/src/app/features/preprints/components/stepper/review-step/review-step.component.ts +++ b/src/app/features/preprints/components/stepper/review-step/review-step.component.ts @@ -31,7 +31,13 @@ import { import { ResourceType } from '@shared/enums'; import { InterpolatePipe } from '@shared/pipes'; import { ToastService } from '@shared/services'; -import { ContributorsSelectors, FetchSelectedSubjects, GetAllContributors, SubjectsSelectors } from '@shared/stores'; +import { + ContributorsSelectors, + FetchSelectedSubjects, + GetBibliographicContributors, + LoadMoreBibliographicContributors, + SubjectsSelectors, +} from '@shared/stores'; import { FetchResourceInstitutions, InstitutionsSelectors } from '@shared/stores/institutions'; @Component({ @@ -60,7 +66,7 @@ export class ReviewStepComponent implements OnInit { private router = inject(Router); private toastService = inject(ToastService); private actions = createDispatchMap({ - getContributors: GetAllContributors, + getBibliographicContributors: GetBibliographicContributors, fetchSubjects: FetchSelectedSubjects, fetchLicenses: FetchLicenses, fetchPreprintProject: FetchPreprintProject, @@ -68,6 +74,7 @@ export class ReviewStepComponent implements OnInit { fetchResourceInstitutions: FetchResourceInstitutions, updatePrimaryFileRelationship: UpdatePrimaryFileRelationship, updatePreprint: UpdatePreprint, + loadMoreBibliographicContributors: LoadMoreBibliographicContributors, }); provider = input.required(); @@ -76,8 +83,9 @@ export class ReviewStepComponent implements OnInit { preprintFile = select(PreprintStepperSelectors.getPreprintFile); isPreprintSubmitting = select(PreprintStepperSelectors.isPreprintSubmitting); - contributors = select(ContributorsSelectors.getContributors); - bibliographicContributors = computed(() => this.contributors().filter((contributor) => contributor.isBibliographic)); + bibliographicContributors = select(ContributorsSelectors.getBibliographicContributors); + areContributorsLoading = select(ContributorsSelectors.isBibliographicContributorsLoading); + hasMoreBibliographicContributors = select(ContributorsSelectors.hasMoreBibliographicContributors); subjects = select(SubjectsSelectors.getSelectedSubjects); affiliatedInstitutions = select(InstitutionsSelectors.getResourceInstitutions); license = select(PreprintStepperSelectors.getPreprintLicense); @@ -88,7 +96,7 @@ export class ReviewStepComponent implements OnInit { readonly PreregLinkInfo = PreregLinkInfo; ngOnInit(): void { - this.actions.getContributors(this.preprint()!.id, ResourceType.Preprint); + this.actions.getBibliographicContributors(this.preprint()?.id, ResourceType.Preprint); this.actions.fetchSubjects(this.preprint()!.id, ResourceType.Preprint); this.actions.fetchLicenses(); this.actions.fetchPreprintProject(); @@ -123,4 +131,8 @@ export class ReviewStepComponent implements OnInit { cancelSubmission() { this.router.navigateByUrl('/preprints'); } + + loadMoreContributors(): void { + this.actions.loadMoreBibliographicContributors(this.preprint()?.id, ResourceType.Preprint); + } } diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index 93a9f8654..2f3ba9325 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -9,7 +9,7 @@ import { PreprintState } from '@osf/features/preprints/store/preprint'; import { PreprintProvidersState } from '@osf/features/preprints/store/preprint-providers'; import { PreprintStepperState } from '@osf/features/preprints/store/preprint-stepper'; import { ConfirmLeavingGuard } from '@shared/guards'; -import { CitationsState, ContributorsState, ProjectsState, SubjectsState } from '@shared/stores'; +import { CitationsState, ProjectsState, SubjectsState } from '@shared/stores'; import { PreprintModerationState } from '../moderation/store/preprint-moderation'; @@ -18,14 +18,7 @@ export const preprintsRoutes: Routes = [ path: '', component: PreprintsComponent, providers: [ - provideStates([ - PreprintProvidersState, - PreprintStepperState, - ContributorsState, - SubjectsState, - PreprintState, - CitationsState, - ]), + provideStates([PreprintProvidersState, PreprintStepperState, SubjectsState, PreprintState, CitationsState]), ], children: [ { diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index f9d5a7850..f65b70d76 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -89,6 +89,10 @@ [isCollectionsRoute]="isCollectionsRoute()" [canEdit]="hasAdminAccess()" [showEditButton]="hasWriteAccess()" + [bibliographicContributors]="bibliographicContributors()" + [isBibliographicContributorsLoading]="isBibliographicContributorsLoading()" + [hasMoreBibliographicContributors]="hasMoreBibliographicContributors()" + (loadMoreContributors)="handleLoadMoreContributors()" />

diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 8c87fc8e0..f53edbfc8 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -39,9 +39,11 @@ import { ClearConfiguredAddons, ClearWiki, CollectionsSelectors, + ContributorsSelectors, CurrentResourceSelectors, FetchSelectedSubjects, GetAddonsResourceReference, + GetBibliographicContributors, GetBookmarksCollectionId, GetCollectionProvider, GetConfiguredCitationAddons, @@ -49,6 +51,8 @@ import { GetHomeWiki, GetLinkedResources, GetResourceWithChildren, + LoadMoreBibliographicContributors, + ResetContributorsState, SubjectsSelectors, } from '@osf/shared/stores'; import { GetActivityLogs } from '@osf/shared/stores/activity-logs'; @@ -139,6 +143,9 @@ export class ProjectOverviewComponent implements OnInit { isWikiEnabled = select(ProjectOverviewSelectors.isWikiEnabled); parentProject = select(ProjectOverviewSelectors.getParentProject); isParentProjectLoading = select(ProjectOverviewSelectors.getParentProjectLoading); + bibliographicContributors = select(ContributorsSelectors.getBibliographicContributors); + isBibliographicContributorsLoading = select(ContributorsSelectors.isBibliographicContributorsLoading); + hasMoreBibliographicContributors = select(ContributorsSelectors.hasMoreBibliographicContributors); addonsResourceReference = select(AddonsSelectors.getAddonsResourceReference); configuredCitationAddons = select(AddonsSelectors.getConfiguredCitationAddons); operationInvocation = select(AddonsSelectors.getOperationInvocation); @@ -164,6 +171,9 @@ export class ProjectOverviewComponent implements OnInit { getParentProject: GetParentProject, getAddonsResourceReference: GetAddonsResourceReference, getConfiguredCitationAddons: GetConfiguredCitationAddons, + getBibliographicContributors: GetBibliographicContributors, + loadMoreBibliographicContributors: LoadMoreBibliographicContributors, + resetContributorsState: ResetContributorsState, }); readonly activityPageSize = 5; @@ -193,8 +203,9 @@ export class ProjectOverviewComponent implements OnInit { resourceOverview = computed(() => { const project = this.currentProject(); const subjects = this.subjects(); + const bibliographicContributors = this.bibliographicContributors(); if (project) { - return MapProjectOverview(project, subjects, this.isAnonymous()); + return MapProjectOverview(project, subjects, this.isAnonymous(), bibliographicContributors); } return null; }); @@ -282,6 +293,10 @@ export class ProjectOverviewComponent implements OnInit { this.actions.setProjectCustomCitation(citation); } + handleLoadMoreContributors(): void { + this.actions.loadMoreBibliographicContributors(this.currentProject()?.id, ResourceType.Project); + } + ngOnInit(): void { const projectId = this.route.snapshot.params['id'] || this.route.parent?.snapshot.params['id']; @@ -291,6 +306,7 @@ export class ProjectOverviewComponent implements OnInit { this.actions.getComponents(projectId); this.actions.getLinkedProjects(projectId); this.actions.getActivityLogs(projectId, this.activityDefaultPage, this.activityPageSize); + this.actions.getBibliographicContributors(projectId, ResourceType.Project); } this.dataciteService @@ -385,6 +401,7 @@ export class ProjectOverviewComponent implements OnInit { this.actions.getComponents(projectId); this.actions.getLinkedProjects(projectId); this.actions.getActivityLogs(projectId, this.activityDefaultPage, this.activityPageSize); + this.actions.getBibliographicContributors(projectId, ResourceType.Project); }), takeUntilDestroyed(this.destroyRef) ) @@ -398,6 +415,7 @@ export class ProjectOverviewComponent implements OnInit { this.actions.clearCollections(); this.actions.clearCollectionModeration(); this.actions.clearConfiguredAddons(); + this.actions.resetContributorsState(); }); } diff --git a/src/app/features/project/overview/services/project-overview.service.ts b/src/app/features/project/overview/services/project-overview.service.ts index 4261b22a8..eb8bf543a 100644 --- a/src/app/features/project/overview/services/project-overview.service.ts +++ b/src/app/features/project/overview/services/project-overview.service.ts @@ -34,14 +34,7 @@ export class ProjectOverviewService { getProjectById(projectId: string): Observable { const params: Record = { - 'embed[]': [ - 'bibliographic_contributors', - 'affiliated_institutions', - 'identifiers', - 'license', - 'storage', - 'preprints', - ], + 'embed[]': ['affiliated_institutions', 'identifiers', 'license', 'storage', 'preprints'], 'fields[institutions]': 'assets,description,name', 'fields[preprints]': 'title,date_created', 'fields[users]': 'family_name,full_name,given_name,middle_name', diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 5d7d3bae8..55951dc01 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -8,7 +8,6 @@ import { LicensesService } from '@osf/shared/services'; import { CitationsState, CollectionsState, - ContributorsState, DuplicatesState, NodeLinksState, SubjectsState, @@ -53,7 +52,7 @@ export const projectRoutes: Routes = [ { path: 'metadata', loadChildren: () => import('@osf/features/metadata/metadata.routes').then((mod) => mod.metadataRoutes), - providers: [provideStates([SubjectsState, ContributorsState])], + providers: [provideStates([SubjectsState])], data: { resourceType: ResourceType.Project }, canActivate: [viewOnlyGuard], }, @@ -87,7 +86,7 @@ export const projectRoutes: Routes = [ canActivate: [viewOnlyGuard], loadComponent: () => import('../contributors/contributors.component').then((mod) => mod.ContributorsComponent), data: { resourceType: ResourceType.Project }, - providers: [provideStates([ContributorsState, ViewOnlyLinkState])], + providers: [provideStates([ViewOnlyLinkState])], }, { path: 'analytics', 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 7726dae69..70164d25d 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 @@ -6,8 +6,9 @@

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

[(contributors)]="contributors" [tableParams]="tableParams()" [isLoading]="isContributorsLoading()" + [isLoadingMore]="isLoadingMore()" (remove)="removeContributor($event)" - (pageChanged)="pageChanged($event)" + (loadMore)="loadMoreContributors()" />
diff --git a/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.spec.ts b/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.spec.ts index 35d007b66..bbef5fbd3 100644 --- a/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.spec.ts +++ b/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.spec.ts @@ -9,8 +9,8 @@ import { ActivatedRoute } from '@angular/router'; import { UserSelectors } from '@core/store/user'; import { ResourceType } from '@osf/shared/enums'; import { CustomConfirmationService, CustomDialogService, ToastService } from '@osf/shared/services'; -import { ContributorsSelectors } from '@osf/shared/stores'; -import { ContributorsTableComponent } from '@shared/components/contributors'; +import { ContributorsSelectors } from '@osf/shared/stores/contributors/contributors.selectors'; +import { ContributorsTableComponent } from '@shared/components/contributors/contributors-table/contributors-table.component'; import { RegistriesContributorsComponent } from './registries-contributors.component'; @@ -77,15 +77,12 @@ describe('RegistriesContributorsComponent', () => { deleteContributor: jest.fn().mockReturnValue(of({})), bulkUpdateContributors: jest.fn().mockReturnValue(of({})), bulkAddContributors: jest.fn().mockReturnValue(of({})), + resetContributorsState: jest.fn().mockRejectedValue(of({})), } as any; Object.defineProperty(component, 'actions', { value: mockActions }); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should request contributors on init', () => { const actions = (component as any).actions; expect(actions.getContributors).toHaveBeenCalledWith('draft-1', ResourceType.DraftRegistration); diff --git a/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.ts b/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.ts index bf07f0974..b7fdc2449 100644 --- a/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.ts +++ b/src/app/features/registries/components/registries-metadata-step/registries-contributors/registries-contributors.component.ts @@ -4,7 +4,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; -import { TableModule, TablePageEvent } from 'primeng/table'; +import { TableModule } from 'primeng/table'; import { filter, map, of } from 'rxjs'; @@ -16,6 +16,7 @@ import { effect, inject, input, + OnDestroy, OnInit, signal, } from '@angular/core'; @@ -40,6 +41,8 @@ import { ContributorsSelectors, DeleteContributor, GetAllContributors, + LoadMoreContributors, + ResetContributorsState, } from '@osf/shared/stores'; @Component({ @@ -49,7 +52,7 @@ import { styleUrl: './registries-contributors.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RegistriesContributorsComponent implements OnInit { +export class RegistriesContributorsComponent implements OnInit, OnDestroy { control = input.required(); readonly destroyRef = inject(DestroyRef); @@ -65,14 +68,15 @@ export class RegistriesContributorsComponent implements OnInit { isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); contributorsTotalCount = select(ContributorsSelectors.getContributorsTotalCount); - page = select(ContributorsSelectors.getContributorsPageNumber); + isLoadingMore = select(ContributorsSelectors.isContributorsLoadingMore); pageSize = select(ContributorsSelectors.getContributorsPageSize); readonly tableParams = computed(() => ({ ...DEFAULT_TABLE_PARAMS, totalRecords: this.contributorsTotalCount(), - paginator: this.contributorsTotalCount() > DEFAULT_TABLE_PARAMS.rows, - firstRowIndex: (this.page() - 1) * this.pageSize(), + paginator: false, + scrollable: true, + firstRowIndex: 0, rows: this.pageSize(), })); @@ -82,6 +86,8 @@ export class RegistriesContributorsComponent implements OnInit { bulkUpdateContributors: BulkUpdateContributors, bulkAddContributors: BulkAddContributors, addContributor: AddContributor, + loadMoreContributors: LoadMoreContributors, + resetContributorsState: ResetContributorsState, }); get hasChanges(): boolean { @@ -98,6 +104,10 @@ export class RegistriesContributorsComponent implements OnInit { this.actions.getContributors(this.draftId(), ResourceType.DraftRegistration); } + ngOnDestroy(): void { + this.actions.resetContributorsState(); + } + onFocusOut() { if (this.control()) { this.control().markAsTouched(); @@ -122,13 +132,10 @@ export class RegistriesContributorsComponent implements OnInit { } openAddContributorDialog() { - const addedContributorIds = this.initialContributors().map((x) => x.userId); - this.customDialogService .open(AddContributorDialogComponent, { header: 'project.contributors.addDialog.addRegisteredContributor', width: '448px', - data: addedContributorIds, }) .onClose.pipe( filter((res: ContributorDialogAddModel) => !!res), @@ -192,10 +199,7 @@ export class RegistriesContributorsComponent implements OnInit { }); } - pageChanged(event: TablePageEvent) { - const page = Math.floor(event.first / event.rows) + 1; - const pageSize = event.rows; - - this.actions.getContributors(this.draftId(), ResourceType.DraftRegistration, page, pageSize); + loadMoreContributors(): void { + this.actions.loadMoreContributors(this.draftId(), ResourceType.DraftRegistration); } } diff --git a/src/app/features/registries/components/review/review.component.html b/src/app/features/registries/components/review/review.component.html index 5f7c247bf..de6695ca7 100644 --- a/src/app/features/registries/components/review/review.component.html +++ b/src/app/features/registries/components/review/review.component.html @@ -28,7 +28,12 @@

{{ 'common.labels.description' | translate }}

{{ 'common.labels.contributors' | translate }}

- +
diff --git a/src/app/features/registries/components/review/review.component.ts b/src/app/features/registries/components/review/review.component.ts index cc8b2e29a..b0ff45a0d 100644 --- a/src/app/features/registries/components/review/review.component.ts +++ b/src/app/features/registries/components/review/review.component.ts @@ -10,7 +10,7 @@ import { Tag } from 'primeng/tag'; import { map, of } from 'rxjs'; -import { ChangeDetectionStrategy, Component, computed, effect, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, OnDestroy } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; @@ -24,6 +24,8 @@ import { ContributorsSelectors, FetchSelectedSubjects, GetAllContributors, + LoadMoreContributors, + ResetContributorsState, SubjectsSelectors, } from '@osf/shared/stores'; @@ -58,7 +60,7 @@ import { SelectComponentsDialogComponent } from '../select-components-dialog/sel styleUrl: './review.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ReviewComponent { +export class ReviewComponent implements OnDestroy { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); private readonly customConfirmationService = inject(CustomConfirmationService); @@ -73,6 +75,8 @@ export class ReviewComponent { readonly stepsData = select(RegistriesSelectors.getStepsData); readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; readonly contributors = select(ContributorsSelectors.getContributors); + readonly areContributorsLoading = select(ContributorsSelectors.isContributorsLoading); + readonly hasMoreContributors = select(ContributorsSelectors.hasMoreContributors); readonly subjects = select(SubjectsSelectors.getSelectedSubjects); readonly components = select(RegistriesSelectors.getRegistrationComponents); readonly license = select(RegistriesSelectors.getRegistrationLicense); @@ -88,6 +92,8 @@ export class ReviewComponent { getProjectsComponents: FetchProjectChildren, fetchLicenses: FetchLicenses, updateStepState: UpdateStepState, + loadMoreContributors: LoadMoreContributors, + resetContributorsState: ResetContributorsState, }); private readonly draftId = toSignal(this.route.params.pipe(map((params) => params['id'])) ?? of(undefined)); @@ -134,6 +140,10 @@ export class ReviewComponent { }); } + ngOnDestroy(): void { + this.actions.resetContributorsState(); + } + goBack(): void { const previousStep = this.pages().length; this.router.navigate(['../', previousStep], { relativeTo: this.route }); @@ -206,4 +216,8 @@ export class ReviewComponent { } }); } + + loadMoreContributors(): void { + this.actions.loadMoreContributors(this.draftId(), ResourceType.DraftRegistration); + } } diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.html b/src/app/features/registry/pages/registry-overview/registry-overview.component.html index 55714a2c2..0a0151a42 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.html +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.html @@ -136,6 +136,10 @@

{{ section.title }}

(customCitationUpdated)="onCustomCitationUpdated($event)" [canEdit]="hasWriteAccess()" [showEditButton]="hasWriteAccess()" + [bibliographicContributors]="bibliographicContributors()" + [isBibliographicContributorsLoading]="isBibliographicContributorsLoading()" + [hasMoreBibliographicContributors]="hasMoreBibliographicContributors()" + (loadMoreContributors)="handleLoadMoreContributors()" />
diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts index af1b77fb6..7df422947 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts @@ -35,7 +35,14 @@ import { hasViewOnlyParam, toCamelCase } from '@osf/shared/helpers'; import { MapRegistryOverview } from '@osf/shared/mappers'; import { SchemaResponse, ToolbarResource } from '@osf/shared/models'; import { CustomDialogService, ToastService } from '@osf/shared/services'; -import { FetchSelectedSubjects, GetBookmarksCollectionId, SubjectsSelectors } from '@osf/shared/stores'; +import { + ContributorsSelectors, + FetchSelectedSubjects, + GetBibliographicContributors, + GetBookmarksCollectionId, + LoadMoreBibliographicContributors, + SubjectsSelectors, +} from '@osf/shared/stores'; import { ArchivingMessageComponent, RegistryRevisionsComponent, RegistryStatusesComponent } from '../../components'; import { RegistryMakeDecisionComponent } from '../../components/registry-make-decision/registry-make-decision.component'; @@ -90,6 +97,9 @@ export class RegistryOverviewComponent { readonly areReviewActionsLoading = select(RegistryOverviewSelectors.areReviewActionsLoading); readonly currentRevision = select(RegistriesSelectors.getSchemaResponse); readonly isSchemaResponseLoading = select(RegistriesSelectors.getSchemaResponseLoading); + bibliographicContributors = select(ContributorsSelectors.getBibliographicContributors); + isBibliographicContributorsLoading = select(ContributorsSelectors.isBibliographicContributorsLoading); + hasMoreBibliographicContributors = select(ContributorsSelectors.hasMoreBibliographicContributors); readonly hasWriteAccess = select(RegistryOverviewSelectors.hasWriteAccess); readonly hasAdminAccess = select(RegistryOverviewSelectors.hasAdminAccess); @@ -180,6 +190,8 @@ export class RegistryOverviewComponent { getRegistryReviewActions: GetRegistryReviewActions, getSchemaResponse: FetchAllSchemaResponses, createSchemaResponse: CreateSchemaResponse, + getBibliographicContributors: GetBibliographicContributors, + loadMoreBibliographicContributors: LoadMoreBibliographicContributors, }); revisionId: string | null = null; @@ -213,6 +225,7 @@ export class RegistryOverviewComponent { effect(() => { if (this.registryId()) { this.actions.getRegistryById(this.registryId()); + this.actions.getBibliographicContributors(this.registryId(), ResourceType.Registration); } }); @@ -272,6 +285,10 @@ export class RegistryOverviewComponent { .subscribe(); } + handleLoadMoreContributors(): void { + this.actions.loadMoreBibliographicContributors(this.registry()?.id, ResourceType.Registration); + } + private navigateToJustificationPage(): void { const revisionId = this.revisionId || this.revisionInProgress?.id; this.router.navigate([`/registries/revisions/${revisionId}/justification`]); diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index f26006bbe..e4161724b 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -5,13 +5,7 @@ import { Routes } from '@angular/router'; import { viewOnlyGuard } from '@osf/core/guards'; import { ResourceType } from '@osf/shared/enums'; import { LicensesService } from '@osf/shared/services'; -import { - CitationsState, - ContributorsState, - DuplicatesState, - SubjectsState, - ViewOnlyLinkState, -} from '@osf/shared/stores'; +import { CitationsState, DuplicatesState, SubjectsState, ViewOnlyLinkState } from '@osf/shared/stores'; import { ActivityLogsState } from '@shared/stores/activity-logs'; import { AnalyticsState } from '../analytics/store'; @@ -52,7 +46,7 @@ export const registryRoutes: Routes = [ { path: 'metadata', loadChildren: () => import('@osf/features/metadata/metadata.routes').then((mod) => mod.metadataRoutes), - providers: [provideStates([SubjectsState, ContributorsState])], + providers: [provideStates([SubjectsState])], data: { resourceType: ResourceType.Registration }, canActivate: [viewOnlyGuard], }, @@ -68,7 +62,7 @@ export const registryRoutes: Routes = [ canActivate: [viewOnlyGuard], loadComponent: () => import('../contributors/contributors.component').then((mod) => mod.ContributorsComponent), data: { resourceType: ResourceType.Registration }, - providers: [provideStates([ContributorsState, ViewOnlyLinkState])], + providers: [provideStates([ViewOnlyLinkState])], }, { path: 'analytics', diff --git a/src/app/shared/components/contributors-list/contributors-list.component.html b/src/app/shared/components/contributors-list/contributors-list.component.html index 7c379d360..814ad1dac 100644 --- a/src/app/shared/components/contributors-list/contributors-list.component.html +++ b/src/app/shared/components/contributors-list/contributors-list.component.html @@ -1,17 +1,34 @@ -
+
@if (anonymous()) {

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

} @else { - @for (contributor of contributors(); track contributor.id) { -
- @if (readonly() || contributor.isUnregisteredContributor || !contributor.id || contributor.deactivated) { - {{ contributor.fullName }}{{ $last ? '' : ',' }} - } @else { - - {{ contributor.fullName }}{{ $last ? '' : ',' }} - - } + @if (isLoading()) { +
+
+ } @else { + @for (contributor of contributors(); track contributor.id) { +
+ @if (readonly() || contributor.isUnregisteredContributor || !contributor.id || contributor.deactivated) { + {{ contributor.fullName }}{{ $last ? '' : ',' }} + } @else { + + {{ contributor.fullName }}{{ $last ? '' : ',' }} + + } +
+ } } }
+ +@if (hasLoadMore()) { +
+ +
+} diff --git a/src/app/shared/components/contributors-list/contributors-list.component.scss b/src/app/shared/components/contributors-list/contributors-list.component.scss index e69de29bb..b9bc65ea4 100644 --- a/src/app/shared/components/contributors-list/contributors-list.component.scss +++ b/src/app/shared/components/contributors-list/contributors-list.component.scss @@ -0,0 +1,3 @@ +:host { + width: 100%; +} diff --git a/src/app/shared/components/contributors-list/contributors-list.component.ts b/src/app/shared/components/contributors-list/contributors-list.component.ts index 89cc6ab67..b84335abc 100644 --- a/src/app/shared/components/contributors-list/contributors-list.component.ts +++ b/src/app/shared/components/contributors-list/contributors-list.component.ts @@ -1,19 +1,26 @@ import { TranslatePipe } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { Button } from 'primeng/button'; +import { Skeleton } from 'primeng/skeleton'; + +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { RouterLink } from '@angular/router'; import { ContributorModel } from '@shared/models'; @Component({ selector: 'osf-contributors-list', - imports: [RouterLink, TranslatePipe], + imports: [RouterLink, TranslatePipe, Skeleton, Button], templateUrl: './contributors-list.component.html', styleUrl: './contributors-list.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ContributorsListComponent { contributors = input.required[]>(); + isLoading = input(false); + hasLoadMore = input(false); readonly = input(false); anonymous = input(false); + + loadMoreContributors = output(); } diff --git a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.html b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.html index 528e586bc..b235be0d0 100644 --- a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.html +++ b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.html @@ -11,7 +11,12 @@ } @else { @for (item of users(); track $index) { } diff --git a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts index 240492015..cc4fb7387 100644 --- a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts +++ b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts @@ -14,6 +14,7 @@ import { Component, computed, DestroyRef, + effect, inject, OnDestroy, OnInit, @@ -84,6 +85,10 @@ export class AddContributorDialogComponent implements OnInit, OnDestroy { readonly hasComponents = computed(() => this.components().length > 0); readonly buttonLabel = computed(() => (this.isComponentsState() ? 'common.buttons.done' : 'common.buttons.next')); + constructor() { + this.setupEffects(); + } + ngOnInit(): void { this.initializeDialogData(); this.setSearchSubscription(); @@ -160,8 +165,10 @@ export class AddContributorDialogComponent implements OnInit, OnDestroy { .filter((c) => c.checked && !c.isCurrent) .map((c) => c.id); + const filteredUsers = this.selectedUsers().filter((user) => !user.disabled); + this.dialogRef.close({ - data: this.selectedUsers(), + data: filteredUsers, type: AddContributorTypeValue, childNodeIds: childNodeIds.length > 0 ? childNodeIds : undefined, } as ContributorDialogAddModel); @@ -189,4 +196,18 @@ export class AddContributorDialogComponent implements OnInit, OnDestroy { this.currentPage.set(1); this.first.set(0); } + + private setupEffects(): void { + effect(() => { + const usersList = this.users(); + + if (usersList.length > 0) { + const checkedUsers = usersList.filter((user) => user.checked); + + if (checkedUsers.length > 0) { + this.selectedUsers.set(checkedUsers); + } + } + }); + } } 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 99e579a57..9d8f37d3f 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 @@ -1,17 +1,9 @@ @@ -182,6 +174,22 @@ } + + @if (showLoadMore() && index === contributors().length - 1) { + + +
+ +
+ + + }
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 467b1b31a..d9776cbbb 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 @@ -3,7 +3,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; import { Skeleton } from 'primeng/skeleton'; -import { TableModule, TablePageEvent } from 'primeng/table'; +import { TableModule } from 'primeng/table'; import { Tooltip } from 'primeng/tooltip'; import { ChangeDetectionStrategy, Component, computed, inject, input, model, output } from '@angular/core'; @@ -41,6 +41,7 @@ import { InfoIconComponent } from '../../info-icon/info-icon.component'; export class ContributorsTableComponent { contributors = model([]); isLoading = input(false); + isLoadingMore = input(false); tableParams = input.required(); showCurator = input(false); showEducation = input(true); @@ -52,7 +53,7 @@ export class ContributorsTableComponent { hasAdminAccess = input(true); remove = output(); - pageChanged = output(); + loadMore = output(); customDialogService = inject(CustomDialogService); @@ -65,6 +66,12 @@ export class ContributorsTableComponent { deactivatedContributors = computed(() => this.contributors().some((contributor) => contributor.deactivated)); + showLoadMore = computed(() => { + const currentLoadedItems = this.contributors().length; + const totalRecords = this.tableParams().totalRecords; + return currentLoadedItems > 0 && currentLoadedItems < totalRecords; + }); + removeContributor(contributor: ContributorModel) { this.remove.emit(contributor); } @@ -91,7 +98,7 @@ export class ContributorsTableComponent { this.contributors.set(reorderedContributors); } - onPageChange(event: TablePageEvent): void { - this.pageChanged.emit(event); + loadMoreItems() { + this.loadMore.emit(); } } diff --git a/src/app/shared/components/resource-metadata/resource-metadata.component.html b/src/app/shared/components/resource-metadata/resource-metadata.component.html index cd4398182..060edb646 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.html +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.html @@ -19,8 +19,11 @@

{{ 'common.labels.contributors' | translate }}

diff --git a/src/app/shared/components/resource-metadata/resource-metadata.component.ts b/src/app/shared/components/resource-metadata/resource-metadata.component.ts index f1ef25b3a..b83423090 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.ts +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.ts @@ -10,7 +10,7 @@ import { Router, RouterLink } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { OverviewCollectionsComponent } from '@osf/features/project/overview/components/overview-collections/overview-collections.component'; import { CurrentResourceType } from '@osf/shared/enums'; -import { ResourceOverview } from '@shared/models'; +import { ContributorModel, ResourceOverview } from '@shared/models'; import { AffiliatedInstitutionsViewComponent } from '../affiliated-institutions-view/affiliated-institutions-view.component'; import { ContributorsListComponent } from '../contributors-list/contributors-list.component'; @@ -44,6 +44,10 @@ export class ResourceMetadataComponent { isCollectionsRoute = input(false); canEdit = input.required(); showEditButton = input(); + bibliographicContributors = input([]); + isBibliographicContributorsLoading = input(false); + hasMoreBibliographicContributors = input(false); + loadMoreContributors = output(); readonly resourceTypes = CurrentResourceType; readonly dateFormat = 'MMM d, y, h:mm a'; diff --git a/src/app/shared/components/truncated-text/truncated-text.component.scss b/src/app/shared/components/truncated-text/truncated-text.component.scss index bb473e7e9..8c983db9c 100644 --- a/src/app/shared/components/truncated-text/truncated-text.component.scss +++ b/src/app/shared/components/truncated-text/truncated-text.component.scss @@ -4,7 +4,6 @@ text-overflow: ellipsis; display: -webkit-box; line-clamp: var(--line-clamp); - line-height: 1.7; -webkit-line-clamp: var(--line-clamp); -webkit-box-orient: vertical; white-space: pre-line; diff --git a/src/app/shared/mappers/collections/collections.mapper.ts b/src/app/shared/mappers/collections/collections.mapper.ts index 7a741e2d0..22969da39 100644 --- a/src/app/shared/mappers/collections/collections.mapper.ts +++ b/src/app/shared/mappers/collections/collections.mapper.ts @@ -37,12 +37,14 @@ export class CollectionsMapper { facebookAppId: response.attributes.facebook_app_id, allowSubmissions: response.attributes.allow_submissions, allowCommenting: response.attributes.allow_commenting, - assets: { - style: response.attributes.assets.style, - squareColorTransparent: response.attributes.assets.square_color_transparent, - squareColorNoTransparent: response.attributes.assets.square_color_no_transparent, - favicon: response.attributes.assets.favicon, - }, + assets: response.attributes.assets + ? { + style: response.attributes.assets.style, + squareColorTransparent: response.attributes.assets.square_color_transparent, + squareColorNoTransparent: response.attributes.assets.square_color_no_transparent, + favicon: response.attributes.assets.favicon, + } + : {}, shareSource: response.attributes.share_source, sharePublishType: response.attributes.share_publish_type, permissions: response.attributes.permissions, diff --git a/src/app/shared/mappers/resource-overview.mappers.ts b/src/app/shared/mappers/resource-overview.mappers.ts index 419e515fa..cb4a69622 100644 --- a/src/app/shared/mappers/resource-overview.mappers.ts +++ b/src/app/shared/mappers/resource-overview.mappers.ts @@ -1,12 +1,13 @@ import { ProjectOverview } from '@osf/features/project/overview/models'; import { RegistryOverview } from '@osf/features/registry/models'; -import { Institution, ResourceOverview, SubjectModel } from '../models'; +import { ContributorModel, Institution, ResourceOverview, SubjectModel } from '../models'; export function MapProjectOverview( project: ProjectOverview, subjects: SubjectModel[], - isAnonymous = false + isAnonymous = false, + bibliographicContributors: ContributorModel[] = [] ): ResourceOverview { return { id: project.id, @@ -35,7 +36,7 @@ export function MapProjectOverview( currentUserIsContributorOrGroupMember: project.currentUserIsContributorOrGroupMember, wikiEnabled: project.wikiEnabled, subjects: subjects, - contributors: project.contributors?.filter(Boolean) || [], + contributors: bibliographicContributors?.filter(Boolean) || [], customCitation: project.customCitation || null, region: project.region || undefined, affiliatedInstitutions: project.affiliatedInstitutions?.filter(Boolean) || undefined, diff --git a/src/app/shared/mappers/view-only-links.mapper.ts b/src/app/shared/mappers/view-only-links.mapper.ts index a168611b2..3d56a1c4d 100644 --- a/src/app/shared/mappers/view-only-links.mapper.ts +++ b/src/app/shared/mappers/view-only-links.mapper.ts @@ -24,7 +24,7 @@ export class ViewOnlyLinksMapper { id: creator?.id || '', fullName: creator?.fullName || '', }, - nodes: item.embeds.nodes.data.map( + nodes: item.embeds?.nodes?.data?.map( (node) => ({ id: node.id, @@ -59,7 +59,7 @@ export class ViewOnlyLinksMapper { id: creator?.id || '', fullName: creator?.fullName || '', }, - nodes: item.embeds.nodes.data.map( + nodes: item.embeds?.nodes?.data?.map( (node) => ({ id: node.id, diff --git a/src/app/shared/models/contributors/contributor-add.model.ts b/src/app/shared/models/contributors/contributor-add.model.ts index 31bc3bf35..9e6a6f6c6 100644 --- a/src/app/shared/models/contributors/contributor-add.model.ts +++ b/src/app/shared/models/contributors/contributor-add.model.ts @@ -5,4 +5,6 @@ export interface ContributorAddModel { fullName?: string; email?: string; index?: number; + checked?: boolean; + disabled?: boolean; } diff --git a/src/app/shared/stores/contributors/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts index 2ee6ccf7a..e2654c6c9 100644 --- a/src/app/shared/stores/contributors/contributors.actions.ts +++ b/src/app/shared/stores/contributors/contributors.actions.ts @@ -128,3 +128,32 @@ export class RejectRequestAccess { public resourceType: ResourceType | undefined ) {} } + +export class GetBibliographicContributors { + static readonly type = '[Contributors] Get Bibliographic Contributors'; + + constructor( + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined, + public page = 1, + public pageSize = DEFAULT_TABLE_PARAMS.rows + ) {} +} + +export class LoadMoreBibliographicContributors { + static readonly type = '[Contributors] Load More Bibliographic Contributors'; + + constructor( + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined + ) {} +} + +export class LoadMoreContributors { + static readonly type = '[Contributors] Load More Contributors'; + + constructor( + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined + ) {} +} diff --git a/src/app/shared/stores/contributors/contributors.model.ts b/src/app/shared/stores/contributors/contributors.model.ts index 5f7c92b61..89d34a7fd 100644 --- a/src/app/shared/stores/contributors/contributors.model.ts +++ b/src/app/shared/stores/contributors/contributors.model.ts @@ -2,16 +2,21 @@ import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants'; import { ContributorAddModel, ContributorModel, RequestAccessModel } from '@osf/shared/models'; import { AsyncStateModel, AsyncStateWithTotalCount } from '@osf/shared/models/store'; -export interface ContributorsListModel extends AsyncStateWithTotalCount { +export interface ContributorsList extends AsyncStateWithTotalCount { + page: number; + pageSize: number; +} + +export interface ContributorsListWithFiltersModel extends ContributorsList { searchValue: string | null; permissionFilter: string | null; bibliographyFilter: boolean | null; - page: number; - pageSize: number; + isLoadingMore: boolean; } export interface ContributorsStateModel { - contributorsList: ContributorsListModel; + contributorsList: ContributorsListWithFiltersModel; + bibliographicContributorsList: ContributorsList; requestAccessList: AsyncStateModel; users: AsyncStateWithTotalCount; } @@ -27,6 +32,16 @@ export const CONTRIBUTORS_STATE_DEFAULTS: ContributorsStateModel = { totalCount: 0, page: 1, pageSize: DEFAULT_TABLE_PARAMS.rows, + isLoadingMore: false, + }, + bibliographicContributorsList: { + data: [], + isLoading: false, + isSubmitting: false, + error: null, + totalCount: 0, + page: 0, + pageSize: DEFAULT_TABLE_PARAMS.rows, }, requestAccessList: { data: [], diff --git a/src/app/shared/stores/contributors/contributors.selectors.ts b/src/app/shared/stores/contributors/contributors.selectors.ts index f3816a2de..57f026482 100644 --- a/src/app/shared/stores/contributors/contributors.selectors.ts +++ b/src/app/shared/stores/contributors/contributors.selectors.ts @@ -30,11 +30,29 @@ export class ContributorsSelectors { @Selector([ContributorsState]) static getBibliographicContributors(state: ContributorsStateModel) { - if (!state?.contributorsList?.data) { + if (!state?.bibliographicContributorsList?.data) { return []; } - return state.contributorsList.data.filter((contributor) => contributor.isBibliographic); + return state.bibliographicContributorsList.data; + } + + @Selector([ContributorsState]) + static isBibliographicContributorsLoading(state: ContributorsStateModel) { + return state?.bibliographicContributorsList?.isLoading || false; + } + + @Selector([ContributorsState]) + static getBibliographicContributorsTotalCount(state: ContributorsStateModel) { + return state?.bibliographicContributorsList?.totalCount || 0; + } + + @Selector([ContributorsState]) + static hasMoreBibliographicContributors(state: ContributorsStateModel) { + return ( + state?.bibliographicContributorsList?.data?.length < state?.bibliographicContributorsList?.totalCount && + !state?.bibliographicContributorsList?.isLoading + ); } @Selector([ContributorsState]) @@ -43,8 +61,8 @@ export class ContributorsSelectors { } @Selector([ContributorsState]) - static getContributorsPageNumber(state: ContributorsStateModel) { - return state.contributorsList.page; + static isContributorsLoadingMore(state: ContributorsStateModel) { + return state?.contributorsList?.isLoadingMore || false; } @Selector([ContributorsState]) @@ -57,6 +75,13 @@ export class ContributorsSelectors { return state.contributorsList.totalCount; } + @Selector([ContributorsState]) + static hasMoreContributors(state: ContributorsStateModel) { + return ( + state?.contributorsList?.data?.length < state?.contributorsList?.totalCount && !state?.contributorsList?.isLoading + ); + } + @Selector([ContributorsState]) static getUsers(state: ContributorsStateModel) { return state?.users?.data || []; diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts index 091cdf130..cd894866a 100644 --- a/src/app/shared/stores/contributors/contributors.state.ts +++ b/src/app/shared/stores/contributors/contributors.state.ts @@ -11,12 +11,15 @@ import { AcceptRequestAccess, AddContributor, BulkAddContributors, - BulkUpdateContributors, BulkAddContributorsFromParentProject, + BulkUpdateContributors, ClearUsers, DeleteContributor, GetAllContributors, + GetBibliographicContributors, GetRequestAccessContributors, + LoadMoreBibliographicContributors, + LoadMoreContributors, RejectRequestAccess, ResetContributorsState, SearchUsers, @@ -49,19 +52,23 @@ export class ContributorsState { ctx.patchState({ contributorsList: { ...state.contributorsList, - data: [], - isLoading: true, + data: page === 1 ? [] : state.contributorsList.data, + isLoading: page === 1, + isLoadingMore: page > 1, error: null, }, }); return this.contributorsService.getAllContributors(action.resourceType, action.resourceId, page, pageSize).pipe( tap((res) => { + const data = page === 1 ? res.data : [...state.contributorsList.data, ...res.data]; + ctx.patchState({ contributorsList: { ...state.contributorsList, - data: res.data, + data, isLoading: false, + isLoadingMore: false, totalCount: res.totalCount, page, pageSize, @@ -283,17 +290,21 @@ export class ContributorsState { users: { ...state.users, isLoading: true, error: null }, }); - const addedContributorsIds = state.contributorsList.data.map((contributor) => contributor.userId); - if (!action.searchValue) { return of([]); } return this.contributorsService.searchUsers(action.searchValue, action.page).pipe( tap((users) => { + const addedContributorsIds = state.contributorsList.data.map((contributor) => contributor.userId); + ctx.patchState({ users: { - data: users.data.filter((user) => !addedContributorsIds.includes(user.id!)), + data: users.data.map((user) => ({ + ...user, + checked: addedContributorsIds.includes(user.id!), + disabled: addedContributorsIds.includes(user.id!), + })), isLoading: false, error: '', totalCount: users.totalCount, @@ -309,6 +320,67 @@ export class ContributorsState { ctx.patchState({ users: { data: [], isLoading: false, error: null, totalCount: 0 } }); } + @Action(GetBibliographicContributors) + getBibliographicContributors(ctx: StateContext, action: GetBibliographicContributors) { + const state = ctx.getState(); + + if (!action.resourceId || !action.resourceType) { + return; + } + + ctx.patchState({ + bibliographicContributorsList: { + ...state.bibliographicContributorsList, + data: action.page === 1 ? [] : state.bibliographicContributorsList.data, + isLoading: true, + error: null, + }, + }); + + return this.contributorsService + .getBibliographicContributors(action.resourceType, action.resourceId, action.page, action.pageSize) + .pipe( + tap((res) => { + const data = action.page === 1 ? res.data : [...state.bibliographicContributorsList.data, ...res.data]; + + ctx.patchState({ + bibliographicContributorsList: { + data, + isLoading: false, + error: null, + page: action.page, + pageSize: res.pageSize, + totalCount: res.totalCount, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'bibliographicContributorsList', error)) + ); + } + + @Action(LoadMoreBibliographicContributors) + loadMoreBibliographicContributors( + ctx: StateContext, + action: LoadMoreBibliographicContributors + ) { + const state = ctx.getState(); + const nextPage = state.bibliographicContributorsList.page + 1; + const nextPageSize = state.bibliographicContributorsList.pageSize; + + return ctx.dispatch( + new GetBibliographicContributors(action.resourceId, action.resourceType, nextPage, nextPageSize) + ); + } + + @Action(LoadMoreContributors) + loadMoreContributors(ctx: StateContext, action: LoadMoreContributors) { + const state = ctx.getState(); + const nextPage = state.contributorsList.page + 1; + const nextPageSize = state.contributorsList.pageSize; + + return ctx.dispatch(new GetAllContributors(action.resourceId, action.resourceType, nextPage, nextPageSize)); + } + @Action(ResetContributorsState) resetState(ctx: StateContext) { ctx.setState({ ...CONTRIBUTORS_STATE_DEFAULTS }); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 7bedfa43c..01f68a556 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -57,7 +57,8 @@ "removeAll": "Remove All", "accept": "Accept", "reject": "Reject", - "loadMore": "Load more" + "loadMore": "Load more", + "seeMore": "See more" }, "accessibility": { "help": "Help", diff --git a/src/styles/overrides/table.scss b/src/styles/overrides/table.scss index a27d956b4..416cee613 100644 --- a/src/styles/overrides/table.scss +++ b/src/styles/overrides/table.scss @@ -85,10 +85,13 @@ p-table { td, th { - background-color: transparent; border-bottom: 1px solid var(--grey-2); } + td { + background-color: transparent; + } + tr { &:hover { background: transparent;