diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts index ba7334d24f..3dc4b6cedc 100644 --- a/src/app/access-control/access-control.module.ts +++ b/src/app/access-control/access-control.module.ts @@ -17,6 +17,7 @@ import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component'; import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component'; import { SearchModule } from '../shared/search/search.module'; +import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module'; /** * Condition for displaying error messages on email form field @@ -27,15 +28,16 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = }; @NgModule({ - imports: [ - CommonModule, - SharedModule, - RouterModule, - AccessControlRoutingModule, - FormModule, - NgbAccordionModule, - SearchModule, - ], + imports: [ + CommonModule, + SharedModule, + RouterModule, + AccessControlRoutingModule, + FormModule, + NgbAccordionModule, + SearchModule, + AccessControlFormModule, + ], exports: [ MembersListComponent, ], diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html index 9851aab835..6ef45cdd5b 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html @@ -51,6 +51,3 @@ - - - diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts index 12cfabae08..c8379b06ee 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts @@ -1,25 +1,82 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { of } from 'rxjs'; +import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; +import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; +import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -describe('BrowseComponent', () => { +describe('BulkAccessBrowseComponent', () => { let component: BulkAccessBrowseComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ BulkAccessBrowseComponent ] - }) - .compileComponents(); - }); + const listID1 = 'id1'; + const value1 = 'Selected object'; + const value2 = 'Another selected object'; + + const selected1 = new SelectableObject(value1); + const selected2 = new SelectableObject(value2); + + const testSelection = { id: listID1, selection: [selected1, selected2] } ; + + const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NgbAccordionModule, + NgbNavModule, + TranslateModule.forRoot() + ], + declarations: [BulkAccessBrowseComponent], + providers: [ { provide: SelectableListService, useValue: selectableListService }, ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); beforeEach(() => { fixture = TestBed.createComponent(BulkAccessBrowseComponent); component = fixture.componentInstance; + (component as any).selectableListService.getSelectableList.and.returnValue(of(testSelection)); fixture.detectChanges(); }); - it('should create', () => { + afterEach(() => { + fixture.destroy(); + component = null; + }); + + it('should create the component', () => { expect(component).toBeTruthy(); }); + + it('should have an initial active nav id of "search"', () => { + expect(component.activateId).toEqual('search'); + }); + + it('should have an initial pagination options object with default values', () => { + expect(component.paginationOptions$.getValue().id).toEqual('bas'); + expect(component.paginationOptions$.getValue().pageSize).toEqual(5); + expect(component.paginationOptions$.getValue().currentPage).toEqual(1); + }); + + it('should have an initial remote data with a paginated list as value', () => { + const list = buildPaginatedList(new PageInfo({ + "elementsPerPage": 5, + "totalElements": 2, + "totalPages": 1, + "currentPage": 1 + }), [selected1, selected2]) ; + const rd = createSuccessfulRemoteDataObject(list); + + expect(component.objectsSelected$.value).toEqual(rd); + }); + }); diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts index 7cbb8740a7..53a7302b46 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; +import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; @@ -67,7 +67,8 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { this.subs.push( this.selectableListService.getSelectableList(this.listId).pipe( distinctUntilChanged(), - map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)) + map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)), + tap(console.log) ).subscribe(this.objectsSelected$) ) } @@ -76,14 +77,12 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { currentPage: this.paginationOptions$.value.currentPage + 1 })); - console.log(this.paginationOptions$.value); } pagePrev() { this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { currentPage: this.paginationOptions$.value.currentPage - 1 })); - console.log(this.paginationOptions$.value); } private calculatePageCount(pageSize, totalCount = 0) { diff --git a/src/app/access-control/bulk-access/bulk-access.component.html b/src/app/access-control/bulk-access/bulk-access.component.html index 12ab88cd1a..aa6c82e133 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.html +++ b/src/app/access-control/bulk-access/bulk-access.component.html @@ -1,8 +1,18 @@ -
- + + +
+ +
+ + +
diff --git a/src/app/access-control/bulk-access/bulk-access.component.spec.ts b/src/app/access-control/bulk-access/bulk-access.component.spec.ts index 61fab1e5a9..3372f98dab 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.spec.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.spec.ts @@ -1,25 +1,115 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; import { BulkAccessComponent } from './bulk-access.component'; +import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Process } from '../../process-page/processes/process.model'; -describe('BulkAccessComponent', () => { +fdescribe('BulkAccessComponent', () => { let component: BulkAccessComponent; let fixture: ComponentFixture; + let bulkAccessControlService: any; + let selectableListService: any; + + const selectableListServiceMock = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']); + const bulkAccessControlServiceMock = jasmine.createSpyObj('bulkAccessControlService', ['createPayloadFile', 'executeScript']); + + const mockFormState = { + "bitstream": [], + "item": [ + { + "name": "embargo", + "startDate": { + "year": 2026, + "month": 5, + "day": 31 + }, + "endDate": null + } + ], + "state": { + "item": { + "toggleStatus": true, + "accessMode": "replace" + }, + "bitstream": { + "toggleStatus": false, + "accessMode": "", + "changesLimit": "", + "selectedBitstreams": [] + } + } + }; + + const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { + getValue: jasmine.createSpy('getValue'), + reset: jasmine.createSpy('reset') + }); + const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }]; + const selectableListState: SelectableListState = { id: 'test', selection }; + const expectedIdList = ['1234', '5678']; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ BulkAccessComponent ] + imports: [ TranslateModule.forRoot() ], + declarations: [ BulkAccessComponent ], + providers: [ + { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, + { provide: SelectableListService, useValue: selectableListServiceMock } + ], + schemas: [NO_ERRORS_SCHEMA] }) - .compileComponents(); + .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(BulkAccessComponent); component = fixture.componentInstance; + bulkAccessControlService = TestBed.inject(BulkAccessControlService); + selectableListService = TestBed.inject(SelectableListService); + (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState)); fixture.detectChanges(); + component.settings = mockSettings; + }); + + afterEach(() => { + fixture.destroy(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should generate the id list by selected elements', () => { + expect(component.objectsSelected$.value).toEqual(expectedIdList); + }); + + it('should disable the execute button when there are no objects selected', () => { + expect(component.canExport()).toBe(false); + }); + + it('should enable the execute button when there are objects selected', () => { + component.objectsSelected$.next(['1234']); + expect(component.canExport()).toBe(true); + }); + + it('should call the settings reset method when reset is called', () => { + spyOn(component.settings, 'reset'); + component.reset(); + expect(component.settings.reset).toHaveBeenCalled(); + }); + + it('should call the bulkAccessControlService executeScript method when submit is called', () => { + (component.settings as any).getValue.and.returnValue(mockFormState) + bulkAccessControlService.executeScript.and.returnValue(createSuccessfulRemoteDataObject$(new Process())); + component.objectsSelected$.next(['1234']); + component.submit(); + expect(bulkAccessControlService.executeScript).toHaveBeenCalled(); + }); }); diff --git a/src/app/access-control/bulk-access/bulk-access.component.ts b/src/app/access-control/bulk-access/bulk-access.component.ts index bc0d293e61..8a82edb273 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.ts @@ -1,8 +1,31 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; + +import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; +import { distinctUntilChanged, map, take, tap } from 'rxjs/operators'; +import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; +import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; @Component({ selector: 'ds-bulk-access', - templateUrl: './bulk-access.component.html', + // templateUrl: './bulk-access.component.html', + template: `
+ +
+ + +
+ +
+ + +
+
`, styleUrls: ['./bulk-access.component.scss'] }) export class BulkAccessComponent implements OnInit { @@ -13,9 +36,78 @@ export class BulkAccessComponent implements OnInit { */ listId: string = 'bulk-access-list'; - constructor() { } + /** + * The list of the objects already selected + */ + objectsSelected$: BehaviorSubject = new BehaviorSubject([]); + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + */ + private subs: Subscription[] = []; + + /** + * The SectionsDirective reference + */ + @ViewChild('dsBulkSettings') settings: BulkAccessSettingsComponent; + + constructor( + private bulkAccessControlService: BulkAccessControlService, + private selectableListService: SelectableListService + ) { } ngOnInit(): void { + this.subs.push( + this.selectableListService.getSelectableList(this.listId).pipe( + distinctUntilChanged(), + tap(console.log), + map((list: SelectableListState) => this.generateIdListBySelectedElements(list)), + tap(console.log) + ).subscribe(this.objectsSelected$) + ) } + canExport(): boolean { + return this.objectsSelected$.value?.length > 0; + } + + /** + * Reset the form to its initial state + * This will also reset the state of the child components (bitstream and item access) + */ + reset(): void { + this.settings.reset() + } + + /** + * Submit the form + * This will create a payload file and execute the script + */ + submit(): void { + const settings = this.settings.getValue(); + const bitstreamAccess = settings.bitstream; + const itemAccess = settings.item; + + const { file } = this.bulkAccessControlService.createPayloadFile({ + bitstreamAccess, + itemAccess, + state: settings.state + }); + + this.bulkAccessControlService.executeScript( + this.objectsSelected$.value || [], + file + ).pipe(take(1)).subscribe((res) => { + console.log('success', res); + }); + } + + /** + * Generate The RemoteData object containing the list of the selected elements + * @param list + * @private + */ + private generateIdListBySelectedElements(list: SelectableListState): string[] { + return list?.selection?.map((entry: any) => entry.indexableObject.uuid); + } } diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html index a5c292eddd..01f36ef03f 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html @@ -15,7 +15,7 @@ -

bulk-access-settings works!

+
diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts index 435f2828b9..306d4eebde 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts @@ -1,25 +1,80 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { BulkAccessSettingsComponent } from './bulk-access-settings.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('BulkAccessSettingsComponent', () => { let component: BulkAccessSettingsComponent; let fixture: ComponentFixture; + const mockFormState = { + "bitstream": [], + "item": [ + { + "name": "embargo", + "startDate": { + "year": 2026, + "month": 5, + "day": 31 + }, + "endDate": null + } + ], + "state": { + "item": { + "toggleStatus": true, + "accessMode": "replace" + }, + "bitstream": { + "toggleStatus": false, + "accessMode": "", + "changesLimit": "", + "selectedBitstreams": [] + } + } + }; + + const mockControl: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { + getFormValue: jasmine.createSpy('getFormValue'), + reset: jasmine.createSpy('reset') + }); beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ BulkAccessSettingsComponent ] - }) - .compileComponents(); + imports: [NgbAccordionModule, TranslateModule.forRoot()], + declarations: [BulkAccessSettingsComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(BulkAccessSettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); + component.controlForm = mockControl; }); - it('should create', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); + + it('should have a method to get the form value', () => { + expect(component.getValue).toBeDefined(); + }); + + it('should have a method to reset the form', () => { + expect(component.reset).toBeDefined(); + }); + + it('should return the correct form value', () => { + const expectedValue = mockFormState; + (component.controlForm as any).getFormValue.and.returnValue(mockFormState) + const actualValue = component.getValue(); + expect(actualValue).toEqual(expectedValue); + }); + + it('should call reset on the control form', () => { + component.reset(); + expect(component.controlForm.reset).toHaveBeenCalled(); + }); }); diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts index cad9e5b981..978fc41ed8 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts @@ -1,20 +1,34 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; +import { + AccessControlFormContainerComponent +} from '../../../shared/access-control-form-container/access-control-form-container.component'; @Component({ selector: 'ds-bulk-access-settings', - templateUrl: './bulk-access-settings.component.html', - styleUrls: ['./bulk-access-settings.component.scss'] + templateUrl: 'bulk-access-settings.component.html', + styleUrls: ['./bulk-access-settings.component.scss'], + exportAs: 'dsBulkSettings' }) -export class BulkAccessSettingsComponent implements OnInit { +export class BulkAccessSettingsComponent { /** - * The selection list id + * The SectionsDirective reference */ - @Input() listId!: string; + @ViewChild('dsAccessControlForm') controlForm: AccessControlFormContainerComponent; - constructor() { } + /** + * Will be used from a parent component to read the value of the form + */ + getValue() { + return this.controlForm.getFormValue(); + } - ngOnInit(): void { + /** + * Reset the form to its initial state + * This will also reset the state of the child components (bitstream and item access) + */ + reset() { + this.controlForm.reset() } } diff --git a/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.spec.ts b/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.spec.ts index 5c97e3abc5..ca715a238c 100644 --- a/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.spec.ts +++ b/src/app/shared/access-control-form-container/access-control-array-form/access-control-array-form.component.spec.ts @@ -11,7 +11,7 @@ import { ControlMaxEndDatePipe } from './control-max-end-date.pipe'; import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; -fdescribe('AccessControlArrayFormComponent', () => { +describe('AccessControlArrayFormComponent', () => { let component: AccessControlArrayFormComponent; let fixture: ComponentFixture; diff --git a/src/app/shared/access-control-form-container/access-control-form-container.component.ts b/src/app/shared/access-control-form-container/access-control-form-container.component.ts index 10ce81d6fc..7e46a53ed5 100644 --- a/src/app/shared/access-control-form-container/access-control-form-container.component.ts +++ b/src/app/shared/access-control-form-container/access-control-form-container.component.ts @@ -65,6 +65,11 @@ export class AccessControlFormContainerComponent impleme * Will be used from a parent component to read the value of the form */ getFormValue() { + console.log({ + bitstream: this.bitstreamAccessCmp.getValue(), + item: this.itemAccessCmp.getValue(), + state: this.state + }); return { bitstream: this.bitstreamAccessCmp.getValue(), item: this.itemAccessCmp.getValue(), diff --git a/src/app/shared/access-control-form-container/bulk-access-control.service.spec.ts b/src/app/shared/access-control-form-container/bulk-access-control.service.spec.ts index 9e733b5694..1bd2efcfef 100644 --- a/src/app/shared/access-control-form-container/bulk-access-control.service.spec.ts +++ b/src/app/shared/access-control-form-container/bulk-access-control.service.spec.ts @@ -1,12 +1,14 @@ import { TestBed } from '@angular/core/testing'; -import { BulkAccessControlService } from './bulk-access-control.service'; +import { BulkAccessControlService, BulkAccessPayload } from './bulk-access-control.service'; import { ScriptDataService } from '../../core/data/processes/script-data.service'; import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; -fdescribe('BulkAccessControlService', () => { +describe('BulkAccessControlService', () => { let service: BulkAccessControlService; let scriptServiceSpy: jasmine.SpyObj; + + beforeEach(() => { const spy = jasmine.createSpyObj('ScriptDataService', ['invoke']); TestBed.configureTestingModule({ @@ -25,7 +27,7 @@ fdescribe('BulkAccessControlService', () => { describe('createPayloadFile', () => { it('should create a file and return the URL and file object', () => { - const payload = { data: 'test' }; + const payload: BulkAccessPayload = { state: null, bitstreamAccess: null, itemAccess: null }; const result = service.createPayloadFile(payload); expect(result.url).toBeTruthy(); diff --git a/src/app/shared/access-control-form-container/bulk-access-control.service.ts b/src/app/shared/access-control-form-container/bulk-access-control.service.ts index f5afe338dd..77fbcbffb2 100644 --- a/src/app/shared/access-control-form-container/bulk-access-control.service.ts +++ b/src/app/shared/access-control-form-container/bulk-access-control.service.ts @@ -4,11 +4,17 @@ import { ScriptDataService } from '../../core/data/processes/script-data.service import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; import { AccessControlFormState } from './access-control-form-container.component'; +export interface BulkAccessPayload { + state: AccessControlFormState; + bitstreamAccess: any; + itemAccess: any; +} + @Injectable({ providedIn: 'root' }) export class BulkAccessControlService { constructor(private scriptService: ScriptDataService) {} - createPayloadFile(payload: { state: AccessControlFormState, bitstreamAccess, itemAccess }) { + createPayloadFile(payload: BulkAccessPayload) { const content = convertToBulkAccessControlFileModel(payload); const blob = new Blob([JSON.stringify(content, null, 2)], { diff --git a/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts b/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts index 1535671f79..496ef28761 100644 --- a/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts +++ b/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts @@ -11,7 +11,7 @@ import { } from './selectable-list.actions'; import { AppState } from '../../../app.reducer'; -class SelectableObject extends ListableObject { +export class SelectableObject extends ListableObject { constructor(private value: string) { super(); }