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",