Skip to content

Commit 5c8828f

Browse files
authored
Merge pull request DSpace#2247 from 4Science/feature/CST-9636
feat: added bulk access control management
2 parents e9b18d8 + 6e6b775 commit 5c8828f

88 files changed

Lines changed: 2540 additions & 87 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/access-control/access-control-routing.module.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
66
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
77
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
88
import { GroupPageGuard } from './group-registry/group-page.guard';
9-
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
10-
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
9+
import {
10+
GroupAdministratorGuard
11+
} from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
12+
import {
13+
SiteAdministratorGuard
14+
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
15+
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
1116

1217
@NgModule({
1318
imports: [
@@ -47,7 +52,16 @@ import { SiteAdministratorGuard } from '../core/data/feature-authorization/featu
4752
},
4853
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
4954
canActivate: [GroupPageGuard]
50-
}
55+
},
56+
{
57+
path: 'bulk-access',
58+
component: BulkAccessComponent,
59+
resolve: {
60+
breadcrumb: I18nBreadcrumbResolver
61+
},
62+
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
63+
canActivate: [SiteAdministratorGuard]
64+
},
5165
])
5266
]
5367
})

src/app/access-control/access-control.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
1212
import { FormModule } from '../shared/form/form.module';
1313
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
1414
import { AbstractControl } from '@angular/forms';
15+
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
16+
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
17+
import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component';
18+
import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component';
19+
import { SearchModule } from '../shared/search/search.module';
20+
import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module';
1521

1622
/**
1723
* Condition for displaying error messages on email form field
@@ -28,6 +34,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
2834
RouterModule,
2935
AccessControlRoutingModule,
3036
FormModule,
37+
NgbAccordionModule,
38+
SearchModule,
39+
AccessControlFormModule,
3140
],
3241
exports: [
3342
MembersListComponent,
@@ -39,6 +48,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
3948
GroupFormComponent,
4049
SubgroupsListComponent,
4150
MembersListComponent,
51+
BulkAccessComponent,
52+
BulkAccessBrowseComponent,
53+
BulkAccessSettingsComponent,
4254
],
4355
providers: [
4456
{
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'">
2+
<ngb-panel [id]="'browse'">
3+
<ng-template ngbPanelHeader>
4+
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')"
5+
data-test="browse">
6+
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()"
7+
[attr.aria-expanded]="!acc.isExpanded('browse')"
8+
aria-controls="collapsePanels">
9+
{{ 'admin.access-control.bulk-access-browse.header' | translate }}
10+
</button>
11+
<div class="text-right d-flex">
12+
<div class="ml-3 d-inline-block">
13+
<span *ngIf="acc.isExpanded('browse')" class="fas fa-chevron-up fa-fw"></span>
14+
<span *ngIf="!acc.isExpanded('browse')" class="fas fa-chevron-down fa-fw"></span>
15+
</div>
16+
</div>
17+
</div>
18+
</ng-template>
19+
<ng-template ngbPanelContent>
20+
<ul ngbNav #nav="ngbNav" [(activeId)]="activateId" class="nav-pills">
21+
<li [ngbNavItem]="'search'">
22+
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
23+
<ng-template ngbNavContent>
24+
<div class="mx-n3">
25+
<ds-themed-search [configuration]="'administrativeBulkAccess'"
26+
[selectable]="true"
27+
[selectionConfig]="{ repeatable: true, listId: listId }"
28+
[showThumbnails]="false"></ds-themed-search>
29+
</div>
30+
</ng-template>
31+
</li>
32+
<li [ngbNavItem]="'selected'">
33+
<a ngbNavLink>
34+
{{'admin.access-control.bulk-access-browse.selected.header' | translate: {number: ((objectsSelected$ | async)?.payload?.totalElements) ? (objectsSelected$ | async)?.payload?.totalElements : '0'} }}
35+
</a>
36+
<ng-template ngbNavContent>
37+
<ds-pagination
38+
[paginationOptions]="(paginationOptions$ | async)"
39+
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo"
40+
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
41+
[objects]="(objectsSelected$|async)"
42+
[showPaginator]="false"
43+
(prev)="pagePrev()"
44+
(next)="pageNext()">
45+
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4">
46+
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
47+
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last '
48+
class="mt-4 mb-4 d-flex"
49+
[attr.data-test]="'list-object' | dsBrowserOnly">
50+
<ds-selectable-list-item-control [index]="i"
51+
[object]="object"
52+
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control>
53+
<ds-listable-object-component-loader [listID]="listId"
54+
[index]="i"
55+
[object]="object"
56+
[showThumbnails]="false"
57+
[viewMode]="'list'"></ds-listable-object-component-loader>
58+
</li>
59+
</ul>
60+
</ds-pagination>
61+
</ng-template>
62+
</li>
63+
</ul>
64+
<div [ngbNavOutlet]="nav" class="mt-5"></div>
65+
</ng-template>
66+
</ngb-panel>
67+
</ngb-accordion>

src/app/access-control/bulk-access/browse/bulk-access-browse.component.scss

Whitespace-only changes.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2+
import { NO_ERRORS_SCHEMA } from '@angular/core';
3+
4+
import { of } from 'rxjs';
5+
import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
6+
import { TranslateModule } from '@ngx-translate/core';
7+
8+
import { BulkAccessBrowseComponent } from './bulk-access-browse.component';
9+
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service';
10+
import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec';
11+
import { PageInfo } from '../../../core/shared/page-info.model';
12+
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
13+
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
14+
15+
describe('BulkAccessBrowseComponent', () => {
16+
let component: BulkAccessBrowseComponent;
17+
let fixture: ComponentFixture<BulkAccessBrowseComponent>;
18+
19+
const listID1 = 'id1';
20+
const value1 = 'Selected object';
21+
const value2 = 'Another selected object';
22+
23+
const selected1 = new SelectableObject(value1);
24+
const selected2 = new SelectableObject(value2);
25+
26+
const testSelection = { id: listID1, selection: [selected1, selected2] } ;
27+
28+
const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']);
29+
beforeEach(waitForAsync(() => {
30+
TestBed.configureTestingModule({
31+
imports: [
32+
NgbAccordionModule,
33+
NgbNavModule,
34+
TranslateModule.forRoot()
35+
],
36+
declarations: [BulkAccessBrowseComponent],
37+
providers: [ { provide: SelectableListService, useValue: selectableListService }, ],
38+
schemas: [
39+
NO_ERRORS_SCHEMA
40+
]
41+
}).compileComponents();
42+
}));
43+
44+
beforeEach(() => {
45+
fixture = TestBed.createComponent(BulkAccessBrowseComponent);
46+
component = fixture.componentInstance;
47+
(component as any).selectableListService.getSelectableList.and.returnValue(of(testSelection));
48+
fixture.detectChanges();
49+
});
50+
51+
afterEach(() => {
52+
fixture.destroy();
53+
component = null;
54+
});
55+
56+
it('should create the component', () => {
57+
expect(component).toBeTruthy();
58+
});
59+
60+
it('should have an initial active nav id of "search"', () => {
61+
expect(component.activateId).toEqual('search');
62+
});
63+
64+
it('should have an initial pagination options object with default values', () => {
65+
expect(component.paginationOptions$.getValue().id).toEqual('bas');
66+
expect(component.paginationOptions$.getValue().pageSize).toEqual(5);
67+
expect(component.paginationOptions$.getValue().currentPage).toEqual(1);
68+
});
69+
70+
it('should have an initial remote data with a paginated list as value', () => {
71+
const list = buildPaginatedList(new PageInfo({
72+
'elementsPerPage': 5,
73+
'totalElements': 2,
74+
'totalPages': 1,
75+
'currentPage': 1
76+
}), [selected1, selected2]) ;
77+
const rd = createSuccessfulRemoteDataObject(list);
78+
79+
expect(component.objectsSelected$.value).toEqual(rd);
80+
});
81+
82+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
2+
3+
import { BehaviorSubject, Subscription } from 'rxjs';
4+
import { distinctUntilChanged, map } from 'rxjs/operators';
5+
6+
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
7+
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
8+
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service';
9+
import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer';
10+
import { RemoteData } from '../../../core/data/remote-data';
11+
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
12+
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
13+
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
14+
import { PageInfo } from '../../../core/shared/page-info.model';
15+
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
16+
import { hasValue } from '../../../shared/empty.util';
17+
18+
@Component({
19+
selector: 'ds-bulk-access-browse',
20+
templateUrl: 'bulk-access-browse.component.html',
21+
styleUrls: ['./bulk-access-browse.component.scss'],
22+
providers: [
23+
{
24+
provide: SEARCH_CONFIG_SERVICE,
25+
useClass: SearchConfigurationService
26+
}
27+
]
28+
})
29+
export class BulkAccessBrowseComponent implements OnInit, OnDestroy {
30+
31+
/**
32+
* The selection list id
33+
*/
34+
@Input() listId!: string;
35+
36+
/**
37+
* The active nav id
38+
*/
39+
activateId = 'search';
40+
41+
/**
42+
* The list of the objects already selected
43+
*/
44+
objectsSelected$: BehaviorSubject<RemoteData<PaginatedList<ListableObject>>> = new BehaviorSubject<RemoteData<PaginatedList<ListableObject>>>(null);
45+
46+
/**
47+
* The pagination options object used for the list of selected elements
48+
*/
49+
paginationOptions$: BehaviorSubject<PaginationComponentOptions> = new BehaviorSubject<PaginationComponentOptions>(Object.assign(new PaginationComponentOptions(), {
50+
id: 'bas',
51+
pageSize: 5,
52+
currentPage: 1
53+
}));
54+
55+
/**
56+
* Array to track all subscriptions and unsubscribe them onDestroy
57+
*/
58+
private subs: Subscription[] = [];
59+
60+
constructor(private selectableListService: SelectableListService) {}
61+
62+
/**
63+
* Subscribe to selectable list updates
64+
*/
65+
ngOnInit(): void {
66+
67+
this.subs.push(
68+
this.selectableListService.getSelectableList(this.listId).pipe(
69+
distinctUntilChanged(),
70+
map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list))
71+
).subscribe(this.objectsSelected$)
72+
);
73+
}
74+
75+
pageNext() {
76+
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
77+
currentPage: this.paginationOptions$.value.currentPage + 1
78+
}));
79+
}
80+
81+
pagePrev() {
82+
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
83+
currentPage: this.paginationOptions$.value.currentPage - 1
84+
}));
85+
}
86+
87+
private calculatePageCount(pageSize, totalCount = 0) {
88+
// we suppose that if we have 0 items we want 1 empty page
89+
return totalCount < pageSize ? 1 : Math.ceil(totalCount / pageSize);
90+
}
91+
92+
/**
93+
* Generate The RemoteData object containing the list of the selected elements
94+
* @param list
95+
* @private
96+
*/
97+
private generatePaginatedListBySelectedElements(list: SelectableListState): RemoteData<PaginatedList<ListableObject>> {
98+
const pageInfo = new PageInfo({
99+
elementsPerPage: this.paginationOptions$.value.pageSize,
100+
totalElements: list?.selection.length,
101+
totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length),
102+
currentPage: this.paginationOptions$.value.currentPage
103+
});
104+
if (pageInfo.currentPage > pageInfo.totalPages) {
105+
pageInfo.currentPage = pageInfo.totalPages;
106+
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
107+
currentPage: pageInfo.currentPage
108+
}));
109+
}
110+
return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || []));
111+
}
112+
113+
ngOnDestroy(): void {
114+
this.subs
115+
.filter((sub) => hasValue(sub))
116+
.forEach((sub) => sub.unsubscribe());
117+
this.selectableListService.deselectAll(this.listId);
118+
}
119+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<div class="container">
2+
<ds-bulk-access-browse [listId]="listId"></ds-bulk-access-browse>
3+
<div class="clearfix mb-3"></div>
4+
<ds-bulk-access-settings #dsBulkSettings ></ds-bulk-access-settings>
5+
6+
<hr>
7+
8+
<div class="d-flex justify-content-end">
9+
<button class="btn btn-outline-primary mr-3" (click)="reset()">
10+
{{ 'access-control-cancel' | translate }}
11+
</button>
12+
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()">
13+
{{ 'access-control-execute' | translate }}
14+
</button>
15+
</div>
16+
</div>
17+
18+
19+

src/app/access-control/bulk-access/bulk-access.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)