+
+
+
+
+ |
+
{{ 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(
|