diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts
index 25754e14a..5c6370d7b 100644
--- a/src/app/features/contributors/contributors.component.ts
+++ b/src/app/features/contributors/contributors.component.ts
@@ -49,6 +49,7 @@ import {
AcceptRequestAccess,
AddContributor,
BulkAddContributors,
+ BulkAddContributorsFromParentProject,
BulkUpdateContributors,
ContributorsSelectors,
CreateViewOnlyLink,
@@ -159,6 +160,7 @@ export class ContributorsComponent implements OnInit {
deleteContributor: DeleteContributor,
bulkUpdateContributors: BulkUpdateContributors,
bulkAddContributors: BulkAddContributors,
+ bulkAddContributorsFromParentProject: BulkAddContributorsFromParentProject,
addContributor: AddContributor,
createViewOnlyLink: CreateViewOnlyLink,
deleteViewOnlyLink: DeleteViewOnlyLink,
@@ -246,17 +248,19 @@ export class ContributorsComponent implements OnInit {
openAddContributorDialog() {
const addedContributorIds = this.initialContributors().map((x) => x.userId);
- const rootParentId = this.resourceDetails().rootParentId ?? this.resourceId();
+ const resourceDetails = this.resourceDetails();
+ const resourceId = this.resourceId();
+ const rootParentId = resourceDetails.rootParentId ?? resourceId;
this.loaderService.show();
this.actions
- .getResourceWithChildren(rootParentId, this.resourceId(), this.resourceType())
+ .getResourceWithChildren(rootParentId, resourceId, this.resourceType())
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.loaderService.hide();
- const components = this.mapNodesToComponentCheckboxItems(this.resourceChildren(), this.resourceId());
+ const components = this.mapNodesToComponentCheckboxItems(this.resourceChildren(), resourceId);
this.customDialogService
.open(AddContributorDialogComponent, {
@@ -265,7 +269,12 @@ export class ContributorsComponent implements OnInit {
data: {
addedContributorIds,
components,
- resourceName: this.resourceDetails().title,
+ resourceName: resourceDetails.title,
+ parentResourceName: resourceDetails.parent?.title,
+ allowAddingContributorsFromParentProject:
+ this.resourceType() === ResourceType.Project &&
+ resourceDetails.rootParentId &&
+ resourceDetails.rootParentId !== resourceId,
},
})
.onClose.pipe(
@@ -273,7 +282,9 @@ export class ContributorsComponent implements OnInit {
takeUntilDestroyed(this.destroyRef)
)
.subscribe((res: ContributorDialogAddModel) => {
- if (res.type === AddContributorType.Unregistered) {
+ if (res.type === AddContributorType.ParentProject) {
+ this.addContributorsFromParentProjectToComponents();
+ } else if (res.type === AddContributorType.Unregistered) {
this.openAddUnregisteredContributorDialog();
} else {
this.addContributorsToComponents(res);
@@ -303,6 +314,13 @@ export class ContributorsComponent implements OnInit {
.subscribe(() => this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'));
}
+ private addContributorsFromParentProjectToComponents(): void {
+ this.actions
+ .bulkAddContributorsFromParentProject(this.resourceId(), this.resourceType())
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'));
+ }
+
openAddUnregisteredContributorDialog() {
this.customDialogService
.open(AddUnregisteredContributorDialogComponent, {
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 933bf2479..528e586bc 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
@@ -65,6 +65,22 @@
}
+ @if (allowAddingContributorsFromParentProject() && this.isSearchState()) {
+
+ }
+
([]);
readonly components = signal([]);
readonly resourceName = signal('');
+ readonly parentResourceName = signal('');
+ readonly allowAddingContributorsFromParentProject = signal(false);
readonly contributorNames = computed(() =>
this.selectedUsers()
@@ -113,6 +115,10 @@ export class AddContributorDialogComponent implements OnInit, OnDestroy {
}
}
+ addSourceProjectContributors(): void {
+ this.closeDialogWithData(AddContributorType.ParentProject);
+ }
+
addUnregistered(): void {
this.dialogRef.close({
data: [],
@@ -129,7 +135,8 @@ export class AddContributorDialogComponent implements OnInit, OnDestroy {
private initializeDialogData(): void {
this.selectedUsers.set([]);
- const { components, resourceName } = this.config.data || {};
+ const { components, resourceName, parentResourceName, allowAddingContributorsFromParentProject } =
+ this.config.data || {};
if (components) {
this.components.set(components);
@@ -138,16 +145,24 @@ export class AddContributorDialogComponent implements OnInit, OnDestroy {
if (resourceName) {
this.resourceName.set(resourceName);
}
+
+ if (allowAddingContributorsFromParentProject) {
+ this.allowAddingContributorsFromParentProject.set(allowAddingContributorsFromParentProject);
+ }
+
+ if (parentResourceName) {
+ this.parentResourceName.set(parentResourceName);
+ }
}
- private closeDialogWithData(): void {
+ private closeDialogWithData(AddContributorTypeValue = AddContributorType.Registered): void {
const childNodeIds = this.components()
.filter((c) => c.checked && !c.isCurrent)
.map((c) => c.id);
this.dialogRef.close({
data: this.selectedUsers(),
- type: AddContributorType.Registered,
+ type: AddContributorTypeValue,
childNodeIds: childNodeIds.length > 0 ? childNodeIds : undefined,
} as ContributorDialogAddModel);
}
diff --git a/src/app/shared/enums/contributors/add-contributor-type.enum.ts b/src/app/shared/enums/contributors/add-contributor-type.enum.ts
index 17ce058fc..cd0e360e4 100644
--- a/src/app/shared/enums/contributors/add-contributor-type.enum.ts
+++ b/src/app/shared/enums/contributors/add-contributor-type.enum.ts
@@ -1,4 +1,5 @@
export enum AddContributorType {
Registered = 1,
Unregistered,
+ ParentProject,
}
diff --git a/src/app/shared/mappers/nodes/base-node.mapper.ts b/src/app/shared/mappers/nodes/base-node.mapper.ts
index 75f0a79b7..771005e2b 100644
--- a/src/app/shared/mappers/nodes/base-node.mapper.ts
+++ b/src/app/shared/mappers/nodes/base-node.mapper.ts
@@ -60,6 +60,7 @@ export class BaseNodeMapper {
wikiEnabled: data.attributes.wiki_enabled,
customCitation: data.attributes.custom_citation || undefined,
rootParentId: data.relationships.root?.data?.id,
+ parent: data.embeds?.parent?.data ? this.getNodeData(data.embeds?.parent.data) : undefined,
};
}
diff --git a/src/app/shared/models/nodes/base-node-embeds-json-api.model.ts b/src/app/shared/models/nodes/base-node-embeds-json-api.model.ts
index 5f7ace796..ae0c580be 100644
--- a/src/app/shared/models/nodes/base-node-embeds-json-api.model.ts
+++ b/src/app/shared/models/nodes/base-node-embeds-json-api.model.ts
@@ -1,4 +1,5 @@
import {
+ BaseNodeDataJsonApi,
ContributorDataJsonApi,
IdentifierAttributes,
IdentifiersJsonApiData,
@@ -23,6 +24,9 @@ export interface BaseNodeEmbedsJsonApi {
region?: {
data: RegionDataJsonApi;
};
+ parent?: {
+ data: BaseNodeDataJsonApi;
+ };
}
export interface JsonApiResource {
diff --git a/src/app/shared/models/nodes/base-node.model.ts b/src/app/shared/models/nodes/base-node.model.ts
index 4187db43e..0ef9ccb0d 100644
--- a/src/app/shared/models/nodes/base-node.model.ts
+++ b/src/app/shared/models/nodes/base-node.model.ts
@@ -23,6 +23,7 @@ export interface BaseNodeModel {
wikiEnabled: boolean;
rootParentId?: string;
type: string;
+ parent?: BaseNodeModel;
}
export interface NodeModel extends BaseNodeModel {
diff --git a/src/app/shared/services/contributors.service.ts b/src/app/shared/services/contributors.service.ts
index 16d525413..7735c7f85 100644
--- a/src/app/shared/services/contributors.service.ts
+++ b/src/app/shared/services/contributors.service.ts
@@ -164,6 +164,12 @@ export class ContributorsService {
.pipe(map((contributor) => ContributorsMapper.getContributor(contributor.data)));
}
+ addContributorsFromProject(resourceType: ResourceType, resourceId: string): Observable {
+ const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/?copy_contributors_from_parent_project=true`;
+ const contributorData = { data: { type: AddContributorType.ParentProject } };
+ return this.jsonApiService.patch(baseUrl, contributorData);
+ }
+
deleteContributor(resourceType: ResourceType, resourceId: string, userId: string): Observable {
const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${userId}/`;
diff --git a/src/app/shared/services/resource.service.ts b/src/app/shared/services/resource.service.ts
index fd227e040..839f2194f 100644
--- a/src/app/shared/services/resource.service.ts
+++ b/src/app/shared/services/resource.service.ts
@@ -66,9 +66,11 @@ export class ResourceGuidService {
getResourceDetails(resourceId: string, resourceType: ResourceType): Observable {
const resourcePath = this.urlMap.get(resourceType);
-
+ const params: Record = {
+ embed: 'parent',
+ };
return this.jsonApiService
- .get>(`${this.apiUrl}/${resourcePath}/${resourceId}/`)
+ .get>(`${this.apiUrl}/${resourcePath}/${resourceId}/`, params)
.pipe(map((response) => BaseNodeMapper.getNodeData(response.data)));
}
diff --git a/src/app/shared/stores/contributors/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts
index 4eaaead98..2ee6ccf7a 100644
--- a/src/app/shared/stores/contributors/contributors.actions.ts
+++ b/src/app/shared/stores/contributors/contributors.actions.ts
@@ -62,6 +62,15 @@ export class BulkAddContributors {
) {}
}
+export class BulkAddContributorsFromParentProject {
+ static readonly type = '[Contributors] Bulk Add Contributors From Parent Project';
+
+ constructor(
+ public resourceId: string | undefined | null,
+ public resourceType: ResourceType | undefined
+ ) {}
+}
+
export class DeleteContributor {
static readonly type = '[Contributors] Delete Contributor';
diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts
index be0259ff5..091cdf130 100644
--- a/src/app/shared/stores/contributors/contributors.state.ts
+++ b/src/app/shared/stores/contributors/contributors.state.ts
@@ -12,6 +12,7 @@ import {
AddContributor,
BulkAddContributors,
BulkUpdateContributors,
+ BulkAddContributorsFromParentProject,
ClearUsers,
DeleteContributor,
GetAllContributors,
@@ -206,6 +207,29 @@ export class ContributorsState {
);
}
+ @Action(BulkAddContributorsFromParentProject)
+ bulkAddContributorsFromParentProject(
+ ctx: StateContext,
+ action: BulkAddContributorsFromParentProject
+ ) {
+ const state = ctx.getState();
+
+ if (!action.resourceId || !action.resourceType) {
+ return;
+ }
+
+ ctx.patchState({
+ contributorsList: { ...state.contributorsList, isLoading: true, error: null },
+ });
+
+ return this.contributorsService.addContributorsFromProject(action.resourceType, action.resourceId).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();
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 8390339d9..7bedfa43c 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -606,7 +606,8 @@
"bibliographicContributor": "Bibliographic Contributor",
"addUnregisteredContributor": "Add Unregistered Contributor",
"addRegisteredContributor": "Add Registered Contributor",
- "unregisteredContributorNotification": "We will notify the user that they have been added to your project"
+ "unregisteredContributorNotification": "We will notify the user that they have been added to your project",
+ "addingContributorsFromParentProject": "Import contributors from {{projectName}}"
},
"removeDialog": {
"title": "Remove contributor",