From aa9d8bf41cd6eaaaab9504eb89579e834eb82b6b Mon Sep 17 00:00:00 2001 From: shernandez-dev Date: Tue, 30 Jun 2026 15:28:09 -0400 Subject: [PATCH 1/4] fix: group collections by community in metadata query collection selector (#5524) --- .../filtered-items.component.html | 164 ++++++++------ .../filtered-items.component.spec.ts | 209 ++++++++++++++++++ .../filtered-items.component.ts | 49 +++- 3 files changed, 347 insertions(+), 75 deletions(-) diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.html b/src/app/admin/admin-reports/filtered-items/filtered-items.component.html index e515999003e..e7664bc9b81 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.html +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.html @@ -12,18 +12,32 @@

{{'admin.reports.items.head' | transl @if (loadingCollections$ | async) { - + } @if ((loadingCollections$ | async) !== true) { - + + } + }
- +
@@ -38,46 +52,48 @@

{{'admin.reports.items.head' | transl
 
@for (pred of queryPredicatesArray().controls; track pred; let i = $index) { - -
+ \ No newline at end of file diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts index e69de29bb2d..75dfc4d2a44 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts @@ -0,0 +1,209 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CollectionDataService } from '@dspace/core/data/collection-data.service'; +import { CommunityDataService } from '@dspace/core/data/community-data.service'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { MetadataFieldDataService } from '@dspace/core/data/metadata-field-data.service'; +import { MetadataSchemaDataService } from '@dspace/core/data/metadata-schema-data.service'; +import { ScriptDataService } from '@dspace/core/data/processes/script-data.service'; +import { DspaceRestService } from '@dspace/core/dspace-rest/dspace-rest.service'; +import { Collection } from '@dspace/core/shared/collection.model'; +import { Community } from '@dspace/core/shared/community.model'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; +import { TranslateModule } from '@ngx-translate/core'; + +import { FilteredItemsComponent } from './filtered-items.component'; + +describe('FilteredItemsComponent', () => { + let component: FilteredItemsComponent; + let fixture: ComponentFixture; + + let communityService: jasmine.SpyObj; + let collectionService: jasmine.SpyObj; + let metadataSchemaService: jasmine.SpyObj; + let metadataFieldService: jasmine.SpyObj; + let scriptDataService: jasmine.SpyObj; + let authorizationDataService: jasmine.SpyObj; + let restService: jasmine.SpyObj; + + const communityA = Object.assign(new Community(), { + uuid: 'community-a', + name: 'Community A', + }); + + const communityB = Object.assign(new Community(), { + uuid: 'community-b', + name: 'Community B', + }); + + const collectionA1 = Object.assign(new Collection(), { + uuid: 'collection-a1', + name: 'Collection A1', + metadata: { + 'dspace.entity.type': [{ value: 'Publication' }], + }, + }); + + const collectionA2NonPub = Object.assign(new Collection(), { + uuid: 'collection-a2', + name: 'Collection A2 (non publication)', + metadata: { + 'dspace.entity.type': [{ value: 'Person' }], + }, + }); + + const collectionB1 = Object.assign(new Collection(), { + uuid: 'collection-b1', + name: 'Collection B1', + metadata: { + 'dspace.entity.type': [{ value: 'Publication' }], + }, + }); + + function initServices(): void { + communityService = jasmine.createSpyObj('communityService', { + findAll: createSuccessfulRemoteDataObject$({ + page: [communityA, communityB], + totalElements: 2, + } as any), + }); + + collectionService = jasmine.createSpyObj('collectionService', { + findByParent: createSuccessfulRemoteDataObject$({ page: [] } as any), + }); + // Different return values per community uuid + collectionService.findByParent.and.callFake((parentUuid: string) => { + if (parentUuid === communityA.uuid) { + return createSuccessfulRemoteDataObject$({ + page: [collectionA1, collectionA2NonPub], + totalElements: 2, + } as any); + } + if (parentUuid === communityB.uuid) { + return createSuccessfulRemoteDataObject$({ + page: [collectionB1], + totalElements: 1, + } as any); + } + return createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any); + }); + + metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', { + findAll: createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any), + }); + + metadataFieldService = jasmine.createSpyObj('metadataFieldService', { + findBySchema: createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any), + }); + + scriptDataService = jasmine.createSpyObj('scriptDataService', { + findById: createSuccessfulRemoteDataObject$(null), + }); + authorizationDataService = jasmine.createSpyObj('authorizationDataService', ['isAuthorized']); + restService = jasmine.createSpyObj('restService', ['request']); + } + + beforeEach(waitForAsync(async () => { + initServices(); + + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ReactiveFormsModule, + FilteredItemsComponent, + ], + providers: [ + { provide: CommunityDataService, useValue: communityService }, + { provide: CollectionDataService, useValue: collectionService }, + { provide: MetadataSchemaDataService, useValue: metadataSchemaService }, + { provide: MetadataFieldDataService, useValue: metadataFieldService }, + { provide: ScriptDataService, useValue: scriptDataService }, + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: DspaceRestService, useValue: restService }, + ], + }).overrideComponent(FilteredItemsComponent, { + remove: { imports: [] }, + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FilteredItemsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('loadCollections', () => { + + it('should populate collectionGroups grouped by community name', () => { + expect(component.collectionGroups.length).toBe(2); + expect(component.collectionGroups[0].communityName).toBe('Community A'); + expect(component.collectionGroups[1].communityName).toBe('Community B'); + }); + + it('should only include collections with dspace.entity.type Publication', () => { + const groupA = component.collectionGroups.find(g => g.communityName === 'Community A'); + expect(groupA.collections.length).toBe(1); + expect(groupA.collections[0].id).toBe('collection-a1'); + }); + + it('should not include communities as selectable collections in the flat collections array', () => { + const communityIds = component.collections.map(c => c.id); + expect(communityIds).not.toContain(communityA.uuid); + expect(communityIds).not.toContain(communityB.uuid); + }); + + it('should include the Whole Repository option in collections', () => { + expect(component.collections.some(c => c.id === '')).toBeTrue(); + }); + + it('should not include groups with zero matching collections', () => { + // No community without any "Publication" collection should appear + const groupNames = component.collectionGroups.map(g => g.communityName); + expect(groupNames).not.toContain('Empty Community'); + }); + + }); + + describe('toggleCollection', () => { + + it('should add a collection id when not previously selected', () => { + component.queryForm.get('collections').setValue([]); + component.toggleCollection('collection-a1'); + expect(component.queryForm.get('collections').value).toEqual(['collection-a1']); + }); + + it('should remove a collection id when already selected', () => { + component.queryForm.get('collections').setValue(['collection-a1', 'collection-b1']); + component.toggleCollection('collection-a1'); + expect(component.queryForm.get('collections').value).toEqual(['collection-b1']); + }); + + it('should allow multiple collections to be selected', () => { + component.queryForm.get('collections').setValue([]); + component.toggleCollection('collection-a1'); + component.toggleCollection('collection-b1'); + expect(component.queryForm.get('collections').value).toEqual(['collection-a1', 'collection-b1']); + }); + + }); + + describe('isCollectionSelected', () => { + + it('should return true when the collection id is in the form value', () => { + component.queryForm.get('collections').setValue(['collection-a1']); + expect(component.isCollectionSelected('collection-a1')).toBeTrue(); + }); + + it('should return false when the collection id is not in the form value', () => { + component.queryForm.get('collections').setValue(['collection-a1']); + expect(component.isCollectionSelected('collection-b1')).toBeFalse(); + }); + + it('should return false when the form value is empty', () => { + component.queryForm.get('collections').setValue([]); + expect(component.isCollectionSelected('collection-a1')).toBeFalse(); + }); + + }); + +}); \ No newline at end of file diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts index 60e1a7f2459..69ba5838c43 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts @@ -1,4 +1,4 @@ -import { AsyncPipe } from '@angular/common'; +import { AsyncPipe, NgClass, } from '@angular/common'; import { Component, OnInit, @@ -53,6 +53,14 @@ import { OptionVO } from './option-vo.model'; import { PresetQuery } from './preset-query.model'; import { QueryPredicate } from './query-predicate.model'; +/** + * Groups collections under their parent community for optgroup rendering + */ +interface CollectionGroup { + communityName: string; + collections: OptionVO[]; +} + /** * Component representing the Filtered Items content report. */ @@ -66,6 +74,7 @@ import { QueryPredicate } from './query-predicate.model'; FilteredItemsExportCsvComponent, FiltersComponent, NgbAccordionModule, + NgClass, ReactiveFormsModule, ThemedLoadingComponent, TranslateModule, @@ -74,6 +83,7 @@ import { QueryPredicate } from './query-predicate.model'; export class FilteredItemsComponent implements OnInit { collections: OptionVO[]; + collectionGroups: CollectionGroup[] = []; /** * A Boolean representing if loading the list of collections is pending */ @@ -104,7 +114,7 @@ export class FilteredItemsComponent implements OnInit { private scriptDataService: ScriptDataService, private authorizationDataService: AuthorizationDataService, private formBuilder: FormBuilder, - private restService: DspaceRestService) {} + private restService: DspaceRestService) { } ngOnInit(): void { this.loadCollections(); @@ -132,6 +142,7 @@ export class FilteredItemsComponent implements OnInit { loadCollections(): void { this.loadingCollections$.next(true); this.collections = []; + this.collectionGroups = []; const wholeRepo$ = this.translateService.stream('admin.reports.items.wholeRepo'); this.collections.push(OptionVO.collectionLoc('', wholeRepo$)); @@ -140,18 +151,24 @@ export class FilteredItemsComponent implements OnInit { ).subscribe( (communitiesRest: Community[]) => { communitiesRest.forEach(community => { - const commVO = OptionVO.collection(community.uuid, community.name, true); - this.collections.push(commVO); - this.collectionService.findByParent(community.uuid, { elementsPerPage: 10000, currentPage: 1 }).pipe( getFirstSucceededRemoteListPayload(), ).subscribe( (collectionsRest: Collection[]) => { - collectionsRest.filter(collection => collection.firstMetadataValue('dspace.entity.type') === 'Publication') - .forEach(collection => { - const collVO = OptionVO.collection(collection.uuid, '–' + collection.name); + const groupCollections: OptionVO[] = collectionsRest + .filter(collection => collection.firstMetadataValue('dspace.entity.type') === 'Publication') + .map(collection => { + const collVO = OptionVO.collection(collection.uuid, collection.name); this.collections.push(collVO); + return collVO; + }); + + if (groupCollections.length > 0) { + this.collectionGroups.push({ + communityName: community.name, + collections: groupCollections, }); + } this.loadingCollections$.next(false); }, ); @@ -160,6 +177,22 @@ export class FilteredItemsComponent implements OnInit { ); } + toggleCollection(collectionId: string): void { + const control = this.queryForm.get('collections'); + const current: string[] = control.value || []; + const index = current.indexOf(collectionId); + if (index > -1) { + control.setValue([...current.slice(0, index), ...current.slice(index + 1)]); + } else { + control.setValue([...current, collectionId]); + } + } + + isCollectionSelected(collectionId: string): boolean { + const current: string[] = this.queryForm.get('collections').value || []; + return current.includes(collectionId); + } + loadPresetQueries(): void { this.presetQueries = [ PresetQuery.of('new', 'admin.reports.items.preset.new', []), From 71a50e81c02797341aa9ec9b39f37a9fc3d77f12 Mon Sep 17 00:00:00 2001 From: shernandez-dev Date: Tue, 30 Jun 2026 17:50:40 -0400 Subject: [PATCH 2/4] fix: replace flat community/collection select with grouped tree selector (#5524) --- .../admin-reports/filtered-items/filtered-items.component.html | 3 ++- .../admin-reports/filtered-items/filtered-items.component.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.html b/src/app/admin/admin-reports/filtered-items/filtered-items.component.html index e7664bc9b81..4203ae2421e 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.html +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.html @@ -19,7 +19,8 @@

{{'admin.reports.items.head' | transl [ngClass]="{'bg-primary text-white': isCollectionSelected('')}" (click)="toggleCollection('')"> {{'admin.reports.items.wholeRepo' | translate}} -
+
@for (group of collectionGroups; track group.communityName) {
{{group.communityName}}
diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts index 69ba5838c43..b407d1374f7 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts @@ -151,7 +151,7 @@ export class FilteredItemsComponent implements OnInit { ).subscribe( (communitiesRest: Community[]) => { communitiesRest.forEach(community => { - this.collectionService.findByParent(community.uuid, { elementsPerPage: 10000, currentPage: 1 }).pipe( + this.collectionService.findByParent(community.uuid, { elementsPerPage: 1000, currentPage: 1 }).pipe( getFirstSucceededRemoteListPayload(), ).subscribe( (collectionsRest: Collection[]) => { From 8f6c2662d161400b2536af52754b94b5af76d838 Mon Sep 17 00:00:00 2001 From: shernandez-dev Date: Tue, 30 Jun 2026 18:20:21 -0400 Subject: [PATCH 3/4] fix: lint - format imports in filtered-items component --- .../filtered-items.component.spec.ts | 390 +++++++++--------- .../filtered-items.component.ts | 5 +- 2 files changed, 201 insertions(+), 194 deletions(-) diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts index 75dfc4d2a44..8f5d966f00a 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts @@ -1,4 +1,8 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { CollectionDataService } from '@dspace/core/data/collection-data.service'; import { CommunityDataService } from '@dspace/core/data/community-data.service'; @@ -15,195 +19,195 @@ import { TranslateModule } from '@ngx-translate/core'; import { FilteredItemsComponent } from './filtered-items.component'; describe('FilteredItemsComponent', () => { - let component: FilteredItemsComponent; - let fixture: ComponentFixture; - - let communityService: jasmine.SpyObj; - let collectionService: jasmine.SpyObj; - let metadataSchemaService: jasmine.SpyObj; - let metadataFieldService: jasmine.SpyObj; - let scriptDataService: jasmine.SpyObj; - let authorizationDataService: jasmine.SpyObj; - let restService: jasmine.SpyObj; - - const communityA = Object.assign(new Community(), { - uuid: 'community-a', - name: 'Community A', - }); - - const communityB = Object.assign(new Community(), { - uuid: 'community-b', - name: 'Community B', - }); - - const collectionA1 = Object.assign(new Collection(), { - uuid: 'collection-a1', - name: 'Collection A1', - metadata: { - 'dspace.entity.type': [{ value: 'Publication' }], - }, - }); - - const collectionA2NonPub = Object.assign(new Collection(), { - uuid: 'collection-a2', - name: 'Collection A2 (non publication)', - metadata: { - 'dspace.entity.type': [{ value: 'Person' }], - }, - }); - - const collectionB1 = Object.assign(new Collection(), { - uuid: 'collection-b1', - name: 'Collection B1', - metadata: { - 'dspace.entity.type': [{ value: 'Publication' }], - }, - }); - - function initServices(): void { - communityService = jasmine.createSpyObj('communityService', { - findAll: createSuccessfulRemoteDataObject$({ - page: [communityA, communityB], - totalElements: 2, - } as any), - }); - - collectionService = jasmine.createSpyObj('collectionService', { - findByParent: createSuccessfulRemoteDataObject$({ page: [] } as any), - }); - // Different return values per community uuid - collectionService.findByParent.and.callFake((parentUuid: string) => { - if (parentUuid === communityA.uuid) { - return createSuccessfulRemoteDataObject$({ - page: [collectionA1, collectionA2NonPub], - totalElements: 2, - } as any); - } - if (parentUuid === communityB.uuid) { - return createSuccessfulRemoteDataObject$({ - page: [collectionB1], - totalElements: 1, - } as any); - } - return createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any); - }); - - metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', { - findAll: createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any), - }); - - metadataFieldService = jasmine.createSpyObj('metadataFieldService', { - findBySchema: createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any), - }); - - scriptDataService = jasmine.createSpyObj('scriptDataService', { - findById: createSuccessfulRemoteDataObject$(null), - }); - authorizationDataService = jasmine.createSpyObj('authorizationDataService', ['isAuthorized']); - restService = jasmine.createSpyObj('restService', ['request']); - } - - beforeEach(waitForAsync(async () => { - initServices(); - - await TestBed.configureTestingModule({ - imports: [ - TranslateModule.forRoot(), - ReactiveFormsModule, - FilteredItemsComponent, - ], - providers: [ - { provide: CommunityDataService, useValue: communityService }, - { provide: CollectionDataService, useValue: collectionService }, - { provide: MetadataSchemaDataService, useValue: metadataSchemaService }, - { provide: MetadataFieldDataService, useValue: metadataFieldService }, - { provide: ScriptDataService, useValue: scriptDataService }, - { provide: AuthorizationDataService, useValue: authorizationDataService }, - { provide: DspaceRestService, useValue: restService }, - ], - }).overrideComponent(FilteredItemsComponent, { - remove: { imports: [] }, - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(FilteredItemsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - describe('loadCollections', () => { - - it('should populate collectionGroups grouped by community name', () => { - expect(component.collectionGroups.length).toBe(2); - expect(component.collectionGroups[0].communityName).toBe('Community A'); - expect(component.collectionGroups[1].communityName).toBe('Community B'); - }); - - it('should only include collections with dspace.entity.type Publication', () => { - const groupA = component.collectionGroups.find(g => g.communityName === 'Community A'); - expect(groupA.collections.length).toBe(1); - expect(groupA.collections[0].id).toBe('collection-a1'); - }); - - it('should not include communities as selectable collections in the flat collections array', () => { - const communityIds = component.collections.map(c => c.id); - expect(communityIds).not.toContain(communityA.uuid); - expect(communityIds).not.toContain(communityB.uuid); - }); - - it('should include the Whole Repository option in collections', () => { - expect(component.collections.some(c => c.id === '')).toBeTrue(); - }); - - it('should not include groups with zero matching collections', () => { - // No community without any "Publication" collection should appear - const groupNames = component.collectionGroups.map(g => g.communityName); - expect(groupNames).not.toContain('Empty Community'); - }); - - }); - - describe('toggleCollection', () => { - - it('should add a collection id when not previously selected', () => { - component.queryForm.get('collections').setValue([]); - component.toggleCollection('collection-a1'); - expect(component.queryForm.get('collections').value).toEqual(['collection-a1']); - }); - - it('should remove a collection id when already selected', () => { - component.queryForm.get('collections').setValue(['collection-a1', 'collection-b1']); - component.toggleCollection('collection-a1'); - expect(component.queryForm.get('collections').value).toEqual(['collection-b1']); - }); - - it('should allow multiple collections to be selected', () => { - component.queryForm.get('collections').setValue([]); - component.toggleCollection('collection-a1'); - component.toggleCollection('collection-b1'); - expect(component.queryForm.get('collections').value).toEqual(['collection-a1', 'collection-b1']); - }); - - }); - - describe('isCollectionSelected', () => { - - it('should return true when the collection id is in the form value', () => { - component.queryForm.get('collections').setValue(['collection-a1']); - expect(component.isCollectionSelected('collection-a1')).toBeTrue(); - }); - - it('should return false when the collection id is not in the form value', () => { - component.queryForm.get('collections').setValue(['collection-a1']); - expect(component.isCollectionSelected('collection-b1')).toBeFalse(); - }); - - it('should return false when the form value is empty', () => { - component.queryForm.get('collections').setValue([]); - expect(component.isCollectionSelected('collection-a1')).toBeFalse(); - }); - - }); - -}); \ No newline at end of file + let component: FilteredItemsComponent; + let fixture: ComponentFixture; + + let communityService: jasmine.SpyObj; + let collectionService: jasmine.SpyObj; + let metadataSchemaService: jasmine.SpyObj; + let metadataFieldService: jasmine.SpyObj; + let scriptDataService: jasmine.SpyObj; + let authorizationDataService: jasmine.SpyObj; + let restService: jasmine.SpyObj; + + const communityA = Object.assign(new Community(), { + uuid: 'community-a', + name: 'Community A', + }); + + const communityB = Object.assign(new Community(), { + uuid: 'community-b', + name: 'Community B', + }); + + const collectionA1 = Object.assign(new Collection(), { + uuid: 'collection-a1', + name: 'Collection A1', + metadata: { + 'dspace.entity.type': [{ value: 'Publication' }], + }, + }); + + const collectionA2NonPub = Object.assign(new Collection(), { + uuid: 'collection-a2', + name: 'Collection A2 (non publication)', + metadata: { + 'dspace.entity.type': [{ value: 'Person' }], + }, + }); + + const collectionB1 = Object.assign(new Collection(), { + uuid: 'collection-b1', + name: 'Collection B1', + metadata: { + 'dspace.entity.type': [{ value: 'Publication' }], + }, + }); + + function initServices(): void { + communityService = jasmine.createSpyObj('communityService', { + findAll: createSuccessfulRemoteDataObject$({ + page: [communityA, communityB], + totalElements: 2, + } as any), + }); + + collectionService = jasmine.createSpyObj('collectionService', { + findByParent: createSuccessfulRemoteDataObject$({ page: [] } as any), + }); + // Different return values per community uuid + collectionService.findByParent.and.callFake((parentUuid: string) => { + if (parentUuid === communityA.uuid) { + return createSuccessfulRemoteDataObject$({ + page: [collectionA1, collectionA2NonPub], + totalElements: 2, + } as any); + } + if (parentUuid === communityB.uuid) { + return createSuccessfulRemoteDataObject$({ + page: [collectionB1], + totalElements: 1, + } as any); + } + return createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any); + }); + + metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', { + findAll: createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any), + }); + + metadataFieldService = jasmine.createSpyObj('metadataFieldService', { + findBySchema: createSuccessfulRemoteDataObject$({ page: [], totalElements: 0 } as any), + }); + + scriptDataService = jasmine.createSpyObj('scriptDataService', { + findById: createSuccessfulRemoteDataObject$(null), + }); + authorizationDataService = jasmine.createSpyObj('authorizationDataService', ['isAuthorized']); + restService = jasmine.createSpyObj('restService', ['request']); + } + + beforeEach(waitForAsync(async () => { + initServices(); + + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ReactiveFormsModule, + FilteredItemsComponent, + ], + providers: [ + { provide: CommunityDataService, useValue: communityService }, + { provide: CollectionDataService, useValue: collectionService }, + { provide: MetadataSchemaDataService, useValue: metadataSchemaService }, + { provide: MetadataFieldDataService, useValue: metadataFieldService }, + { provide: ScriptDataService, useValue: scriptDataService }, + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: DspaceRestService, useValue: restService }, + ], + }).overrideComponent(FilteredItemsComponent, { + remove: { imports: [] }, + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FilteredItemsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('loadCollections', () => { + + it('should populate collectionGroups grouped by community name', () => { + expect(component.collectionGroups.length).toBe(2); + expect(component.collectionGroups[0].communityName).toBe('Community A'); + expect(component.collectionGroups[1].communityName).toBe('Community B'); + }); + + it('should only include collections with dspace.entity.type Publication', () => { + const groupA = component.collectionGroups.find(g => g.communityName === 'Community A'); + expect(groupA.collections.length).toBe(1); + expect(groupA.collections[0].id).toBe('collection-a1'); + }); + + it('should not include communities as selectable collections in the flat collections array', () => { + const communityIds = component.collections.map(c => c.id); + expect(communityIds).not.toContain(communityA.uuid); + expect(communityIds).not.toContain(communityB.uuid); + }); + + it('should include the Whole Repository option in collections', () => { + expect(component.collections.some(c => c.id === '')).toBeTrue(); + }); + + it('should not include groups with zero matching collections', () => { + // No community without any "Publication" collection should appear + const groupNames = component.collectionGroups.map(g => g.communityName); + expect(groupNames).not.toContain('Empty Community'); + }); + + }); + + describe('toggleCollection', () => { + + it('should add a collection id when not previously selected', () => { + component.queryForm.get('collections').setValue([]); + component.toggleCollection('collection-a1'); + expect(component.queryForm.get('collections').value).toEqual(['collection-a1']); + }); + + it('should remove a collection id when already selected', () => { + component.queryForm.get('collections').setValue(['collection-a1', 'collection-b1']); + component.toggleCollection('collection-a1'); + expect(component.queryForm.get('collections').value).toEqual(['collection-b1']); + }); + + it('should allow multiple collections to be selected', () => { + component.queryForm.get('collections').setValue([]); + component.toggleCollection('collection-a1'); + component.toggleCollection('collection-b1'); + expect(component.queryForm.get('collections').value).toEqual(['collection-a1', 'collection-b1']); + }); + + }); + + describe('isCollectionSelected', () => { + + it('should return true when the collection id is in the form value', () => { + component.queryForm.get('collections').setValue(['collection-a1']); + expect(component.isCollectionSelected('collection-a1')).toBeTrue(); + }); + + it('should return false when the collection id is not in the form value', () => { + component.queryForm.get('collections').setValue(['collection-a1']); + expect(component.isCollectionSelected('collection-b1')).toBeFalse(); + }); + + it('should return false when the form value is empty', () => { + component.queryForm.get('collections').setValue([]); + expect(component.isCollectionSelected('collection-a1')).toBeFalse(); + }); + + }); + +}); diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts index b407d1374f7..95f1ecebb0a 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts @@ -1,4 +1,7 @@ -import { AsyncPipe, NgClass, } from '@angular/common'; +import { + AsyncPipe, + NgClass, +} from '@angular/common'; import { Component, OnInit, From 16b35536bf4675a4c9a8b240659446bd720dd1eb Mon Sep 17 00:00:00 2001 From: shernandez-dev Date: Tue, 30 Jun 2026 19:32:25 -0400 Subject: [PATCH 4/4] fix: make Whole Repository and specific collections mutually exclusive (#5883) --- .../filtered-items.component.spec.ts | 18 ++++++++++++++++++ .../filtered-items.component.ts | 19 ++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts index 8f5d966f00a..9fc50d240e3 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts @@ -189,6 +189,24 @@ describe('FilteredItemsComponent', () => { expect(component.queryForm.get('collections').value).toEqual(['collection-a1', 'collection-b1']); }); + it('should deselect specific collections when Whole Repository is selected', () => { + component.queryForm.get('collections').setValue(['collection-a1', 'collection-b1']); + component.toggleCollection(''); + expect(component.queryForm.get('collections').value).toEqual(['']); + }); + + it('should deselect Whole Repository when a specific collection is selected', () => { + component.queryForm.get('collections').setValue(['']); + component.toggleCollection('collection-a1'); + expect(component.queryForm.get('collections').value).toEqual(['collection-a1']); + }); + + it('should deselect Whole Repository when toggled twice', () => { + component.queryForm.get('collections').setValue(['']); + component.toggleCollection(''); + expect(component.queryForm.get('collections').value).toEqual([]); + }); + }); describe('isCollectionSelected', () => { diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts index 95f1ecebb0a..11e96ffe4a8 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts @@ -182,13 +182,22 @@ export class FilteredItemsComponent implements OnInit { toggleCollection(collectionId: string): void { const control = this.queryForm.get('collections'); - const current: string[] = control.value || []; - const index = current.indexOf(collectionId); - if (index > -1) { - control.setValue([...current.slice(0, index), ...current.slice(index + 1)]); + let current: string[] = control.value || []; + + if (collectionId === '') { + // Selecting "Whole Repository" clears any specific collection selection + current = current.includes('') ? [] : ['']; } else { - control.setValue([...current, collectionId]); + // Selecting a specific collection clears "Whole Repository" if selected + current = current.filter(id => id !== ''); + const index = current.indexOf(collectionId); + if (index > -1) { + current = [...current.slice(0, index), ...current.slice(index + 1)]; + } else { + current = [...current, collectionId]; + } } + control.setValue(current); } isCollectionSelected(collectionId: string): boolean {