forked from DSpace/dspace-angular
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request DSpace#2247 from 4Science/feature/CST-9636
feat: added bulk access control management
- Loading branch information
Showing
88 changed files
with
2,540 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
src/app/access-control/bulk-access/browse/bulk-access-browse.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'"> | ||
<ngb-panel [id]="'browse'"> | ||
<ng-template ngbPanelHeader> | ||
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')" | ||
data-test="browse"> | ||
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()" | ||
[attr.aria-expanded]="!acc.isExpanded('browse')" | ||
aria-controls="collapsePanels"> | ||
{{ 'admin.access-control.bulk-access-browse.header' | translate }} | ||
</button> | ||
<div class="text-right d-flex"> | ||
<div class="ml-3 d-inline-block"> | ||
<span *ngIf="acc.isExpanded('browse')" class="fas fa-chevron-up fa-fw"></span> | ||
<span *ngIf="!acc.isExpanded('browse')" class="fas fa-chevron-down fa-fw"></span> | ||
</div> | ||
</div> | ||
</div> | ||
</ng-template> | ||
<ng-template ngbPanelContent> | ||
<ul ngbNav #nav="ngbNav" [(activeId)]="activateId" class="nav-pills"> | ||
<li [ngbNavItem]="'search'"> | ||
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a> | ||
<ng-template ngbNavContent> | ||
<div class="mx-n3"> | ||
<ds-themed-search [configuration]="'administrativeBulkAccess'" | ||
[selectable]="true" | ||
[selectionConfig]="{ repeatable: true, listId: listId }" | ||
[showThumbnails]="false"></ds-themed-search> | ||
</div> | ||
</ng-template> | ||
</li> | ||
<li [ngbNavItem]="'selected'"> | ||
<a ngbNavLink> | ||
{{'admin.access-control.bulk-access-browse.selected.header' | translate: {number: ((objectsSelected$ | async)?.payload?.totalElements) ? (objectsSelected$ | async)?.payload?.totalElements : '0'} }} | ||
</a> | ||
<ng-template ngbNavContent> | ||
<ds-pagination | ||
[paginationOptions]="(paginationOptions$ | async)" | ||
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo" | ||
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements" | ||
[objects]="(objectsSelected$|async)" | ||
[showPaginator]="false" | ||
(prev)="pagePrev()" | ||
(next)="pageNext()"> | ||
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4"> | ||
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize, | ||
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last ' | ||
class="mt-4 mb-4 d-flex" | ||
[attr.data-test]="'list-object' | dsBrowserOnly"> | ||
<ds-selectable-list-item-control [index]="i" | ||
[object]="object" | ||
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control> | ||
<ds-listable-object-component-loader [listID]="listId" | ||
[index]="i" | ||
[object]="object" | ||
[showThumbnails]="false" | ||
[viewMode]="'list'"></ds-listable-object-component-loader> | ||
</li> | ||
</ul> | ||
</ds-pagination> | ||
</ng-template> | ||
</li> | ||
</ul> | ||
<div [ngbNavOutlet]="nav" class="mt-5"></div> | ||
</ng-template> | ||
</ngb-panel> | ||
</ngb-accordion> |
Empty file.
82 changes: 82 additions & 0 deletions
82
src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
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('BulkAccessBrowseComponent', () => { | ||
let component: BulkAccessBrowseComponent; | ||
let fixture: ComponentFixture<BulkAccessBrowseComponent>; | ||
|
||
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(); | ||
}); | ||
|
||
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); | ||
}); | ||
|
||
}); |
119 changes: 119 additions & 0 deletions
119
src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; | ||
|
||
import { BehaviorSubject, Subscription } from 'rxjs'; | ||
import { distinctUntilChanged, map } from 'rxjs/operators'; | ||
|
||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; | ||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; | ||
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; | ||
import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer'; | ||
import { RemoteData } from '../../../core/data/remote-data'; | ||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; | ||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; | ||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; | ||
import { PageInfo } from '../../../core/shared/page-info.model'; | ||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; | ||
import { hasValue } from '../../../shared/empty.util'; | ||
|
||
@Component({ | ||
selector: 'ds-bulk-access-browse', | ||
templateUrl: 'bulk-access-browse.component.html', | ||
styleUrls: ['./bulk-access-browse.component.scss'], | ||
providers: [ | ||
{ | ||
provide: SEARCH_CONFIG_SERVICE, | ||
useClass: SearchConfigurationService | ||
} | ||
] | ||
}) | ||
export class BulkAccessBrowseComponent implements OnInit, OnDestroy { | ||
|
||
/** | ||
* The selection list id | ||
*/ | ||
@Input() listId!: string; | ||
|
||
/** | ||
* The active nav id | ||
*/ | ||
activateId = 'search'; | ||
|
||
/** | ||
* The list of the objects already selected | ||
*/ | ||
objectsSelected$: BehaviorSubject<RemoteData<PaginatedList<ListableObject>>> = new BehaviorSubject<RemoteData<PaginatedList<ListableObject>>>(null); | ||
|
||
/** | ||
* The pagination options object used for the list of selected elements | ||
*/ | ||
paginationOptions$: BehaviorSubject<PaginationComponentOptions> = new BehaviorSubject<PaginationComponentOptions>(Object.assign(new PaginationComponentOptions(), { | ||
id: 'bas', | ||
pageSize: 5, | ||
currentPage: 1 | ||
})); | ||
|
||
/** | ||
* Array to track all subscriptions and unsubscribe them onDestroy | ||
*/ | ||
private subs: Subscription[] = []; | ||
|
||
constructor(private selectableListService: SelectableListService) {} | ||
|
||
/** | ||
* Subscribe to selectable list updates | ||
*/ | ||
ngOnInit(): void { | ||
|
||
this.subs.push( | ||
this.selectableListService.getSelectableList(this.listId).pipe( | ||
distinctUntilChanged(), | ||
map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)) | ||
).subscribe(this.objectsSelected$) | ||
); | ||
} | ||
|
||
pageNext() { | ||
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { | ||
currentPage: this.paginationOptions$.value.currentPage + 1 | ||
})); | ||
} | ||
|
||
pagePrev() { | ||
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { | ||
currentPage: this.paginationOptions$.value.currentPage - 1 | ||
})); | ||
} | ||
|
||
private calculatePageCount(pageSize, totalCount = 0) { | ||
// we suppose that if we have 0 items we want 1 empty page | ||
return totalCount < pageSize ? 1 : Math.ceil(totalCount / pageSize); | ||
} | ||
|
||
/** | ||
* Generate The RemoteData object containing the list of the selected elements | ||
* @param list | ||
* @private | ||
*/ | ||
private generatePaginatedListBySelectedElements(list: SelectableListState): RemoteData<PaginatedList<ListableObject>> { | ||
const pageInfo = new PageInfo({ | ||
elementsPerPage: this.paginationOptions$.value.pageSize, | ||
totalElements: list?.selection.length, | ||
totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length), | ||
currentPage: this.paginationOptions$.value.currentPage | ||
}); | ||
if (pageInfo.currentPage > pageInfo.totalPages) { | ||
pageInfo.currentPage = pageInfo.totalPages; | ||
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { | ||
currentPage: pageInfo.currentPage | ||
})); | ||
} | ||
return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || [])); | ||
} | ||
|
||
ngOnDestroy(): void { | ||
this.subs | ||
.filter((sub) => hasValue(sub)) | ||
.forEach((sub) => sub.unsubscribe()); | ||
this.selectableListService.deselectAll(this.listId); | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/app/access-control/bulk-access/bulk-access.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<div class="container"> | ||
<ds-bulk-access-browse [listId]="listId"></ds-bulk-access-browse> | ||
<div class="clearfix mb-3"></div> | ||
<ds-bulk-access-settings #dsBulkSettings ></ds-bulk-access-settings> | ||
|
||
<hr> | ||
|
||
<div class="d-flex justify-content-end"> | ||
<button class="btn btn-outline-primary mr-3" (click)="reset()"> | ||
{{ 'access-control-cancel' | translate }} | ||
</button> | ||
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()"> | ||
{{ 'access-control-execute' | translate }} | ||
</button> | ||
</div> | ||
</div> | ||
|
||
|
||
|
Empty file.
Oops, something went wrong.