Merge pull request #2247 from 4Science/feature/CST-9636

feat: added bulk access control management
This commit is contained in:
Tim Donohue
2023-06-16 10:48:24 -05:00
committed by GitHub
88 changed files with 2540 additions and 87 deletions

View File

@@ -6,8 +6,13 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { GroupPageGuard } from './group-registry/group-page.guard';
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import {
GroupAdministratorGuard
} from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import {
SiteAdministratorGuard
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
@NgModule({
imports: [
@@ -47,7 +52,16 @@ import { SiteAdministratorGuard } from '../core/data/feature-authorization/featu
},
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
canActivate: [GroupPageGuard]
}
},
{
path: 'bulk-access',
component: BulkAccessComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
canActivate: [SiteAdministratorGuard]
},
])
]
})

View File

@@ -12,6 +12,12 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
import { FormModule } from '../shared/form/form.module';
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
import { AbstractControl } from '@angular/forms';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
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
@@ -28,6 +34,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
RouterModule,
AccessControlRoutingModule,
FormModule,
NgbAccordionModule,
SearchModule,
AccessControlFormModule,
],
exports: [
MembersListComponent,
@@ -39,6 +48,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
GroupFormComponent,
SubgroupsListComponent,
MembersListComponent,
BulkAccessComponent,
BulkAccessBrowseComponent,
BulkAccessSettingsComponent,
],
providers: [
{

View File

@@ -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>

View File

@@ -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);
});
});

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -0,0 +1,158 @@
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';
import { RouterTestingModule } from '@angular/router/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
describe('BulkAccessComponent', () => {
let component: BulkAccessComponent;
let fixture: ComponentFixture<BulkAccessComponent>;
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 mockFile = {
'uuids': [
'1234', '5678'
],
'file': { }
};
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'];
const selectableListStateEmpty: SelectableListState = { id: 'test', selection: [] };
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot()
],
declarations: [ BulkAccessComponent ],
providers: [
{ provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock },
{ provide: NotificationsService, useValue: NotificationsServiceStub },
{ provide: SelectableListService, useValue: selectableListServiceMock }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BulkAccessComponent);
component = fixture.componentInstance;
bulkAccessControlService = TestBed.inject(BulkAccessControlService);
selectableListService = TestBed.inject(SelectableListService);
});
afterEach(() => {
fixture.destroy();
});
describe('when there are no elements selected', () => {
beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty));
fixture.detectChanges();
component.settings = mockSettings;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should generate the id list by selected elements', () => {
expect(component.objectsSelected$.value).toEqual([]);
});
it('should disable the execute button when there are no objects selected', () => {
expect(component.canExport()).toBe(false);
});
});
describe('when there are elements selected', () => {
beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
fixture.detectChanges();
component.settings = mockSettings;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should generate the id list by selected elements', () => {
expect(component.objectsSelected$.value).toEqual(expectedIdList);
});
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', () => {
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.createPayloadFile.and.returnValue(mockFile);
bulkAccessControlService.executeScript.and.returnValue(createSuccessfulRemoteDataObject$(new Process()));
component.objectsSelected$.next(['1234']);
component.submit();
expect(bulkAccessControlService.executeScript).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,94 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
@Component({
selector: 'ds-bulk-access',
templateUrl: './bulk-access.component.html',
styleUrls: ['./bulk-access.component.scss']
})
export class BulkAccessComponent implements OnInit {
/**
* The selection list id
*/
listId = 'bulk-access-list';
/**
* The list of the objects already selected
*/
objectsSelected$: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
/**
* 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(),
map((list: SelectableListState) => this.generateIdListBySelectedElements(list))
).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
).subscribe();
}
/**
* 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);
}
}

View File

@@ -0,0 +1,21 @@
<ngb-accordion #acc="ngbAccordion" [activeIds]="'settings'">
<ngb-panel [id]="'settings'">
<ng-template ngbPanelHeader>
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('settings')" data-test="settings">
<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-settings.header' | translate }}
</button>
<div class="text-right d-flex">
<div class="ml-3 d-inline-block">
<span *ngIf="acc.isExpanded('settings')" class="fas fa-chevron-up fa-fw"></span>
<span *ngIf="!acc.isExpanded('settings')" class="fas fa-chevron-down fa-fw"></span>
</div>
</div>
</div>
</ng-template>
<ng-template ngbPanelContent>
<ds-access-control-form-container #dsAccessControlForm [showSubmit]="false"></ds-access-control-form-container>
</ng-template>
</ngb-panel>
</ngb-accordion>

View File

@@ -0,0 +1,81 @@
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<BulkAccessSettingsComponent>;
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({
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 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();
// @ts-ignore
expect(actualValue).toEqual(expectedValue);
});
it('should call reset on the control form', () => {
component.reset();
expect(component.controlForm.reset).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,34 @@
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'],
exportAs: 'dsBulkSettings'
})
export class BulkAccessSettingsComponent {
/**
* The SectionsDirective reference
*/
@ViewChild('dsAccessControlForm') controlForm: AccessControlFormContainerComponent<any>;
/**
* Will be used from a parent component to read the value of the form
*/
getValue() {
return this.controlForm.getFormValue();
}
/**
* 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();
}
}

View File

@@ -0,0 +1,7 @@
<ds-access-control-form-container
titleMessage="collection-access-control-title"
*ngIf="itemRD$ | async as itemRD"
[itemRD]="itemRD"
[showLimitToSpecificBitstreams]="false">
</ds-access-control-form-container>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CollectionAccessControlComponent } from './collection-access-control.component';
xdescribe('CollectionAccessControlComponent', () => {
let component: CollectionAccessControlComponent;
let fixture: ComponentFixture<CollectionAccessControlComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CollectionAccessControlComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CollectionAccessControlComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { Community } from '../../../core/shared/community.model';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
@Component({
selector: 'ds-collection-access-control',
templateUrl: './collection-access-control.component.html',
styleUrls: ['./collection-access-control.component.scss'],
})
export class CollectionAccessControlComponent implements OnInit {
itemRD$: Observable<RemoteData<Community>>;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.itemRD$ = this.route.parent.parent.data.pipe(
map((data) => data.dso)
).pipe(getFirstSucceededRemoteData()) as Observable<RemoteData<Community>>;
}
}

View File

@@ -9,10 +9,14 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
import { CollectionSourceComponent } from './collection-source/collection-source.component';
import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component';
import { CollectionFormModule } from '../collection-form/collection-form.module';
import { CollectionSourceControlsComponent } from './collection-source/collection-source-controls/collection-source-controls.component';
import {
CollectionSourceControlsComponent
} from './collection-source/collection-source-controls/collection-source-controls.component';
import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module';
import { FormModule } from '../../shared/form/form.module';
import { ComcolModule } from '../../shared/comcol/comcol.module';
import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component';
import { AccessControlFormModule } from '../../shared/access-control-form-container/access-control-form.module';
/**
* Module that contains all components related to the Edit Collection page administrator functionality
@@ -26,6 +30,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
ResourcePoliciesModule,
FormModule,
ComcolModule,
AccessControlFormModule,
],
declarations: [
EditCollectionPageComponent,
@@ -33,7 +38,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
CollectionRolesComponent,
CollectionCurateComponent,
CollectionSourceComponent,
CollectionAccessControlComponent,
CollectionSourceControlsComponent,
CollectionAuthorizationsComponent
]

View File

@@ -13,6 +13,7 @@ import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/cr
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard';
import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component';
/**
* Routing module that handles the routing for the Edit Collection page administrator functionality
@@ -58,6 +59,11 @@ import { CollectionAdministratorGuard } from '../../core/data/feature-authorizat
component: CollectionCurateComponent,
data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true }
},
{
path: 'access-control',
component: CollectionAccessControlComponent,
data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true }
},
/* {
path: 'authorizations',
component: CollectionAuthorizationsComponent,

View File

@@ -0,0 +1,6 @@
<ds-access-control-form-container
titleMessage="community-access-control-title"
*ngIf="itemRD$ | async as itemRD"
[itemRD]="itemRD"
[showLimitToSpecificBitstreams]="false">
</ds-access-control-form-container>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CommunityAccessControlComponent } from './community-access-control.component';
xdescribe('CommunityAccessControlComponent', () => {
let component: CommunityAccessControlComponent;
let fixture: ComponentFixture<CommunityAccessControlComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CommunityAccessControlComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CommunityAccessControlComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { Community } from '../../../core/shared/community.model';
@Component({
selector: 'ds-community-access-control',
templateUrl: './community-access-control.component.html',
styleUrls: ['./community-access-control.component.scss'],
})
export class CommunityAccessControlComponent implements OnInit {
itemRD$: Observable<RemoteData<Community>>;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.itemRD$ = this.route.parent.parent.data.pipe(
map((data) => data.dso)
).pipe(getFirstSucceededRemoteData()) as Observable<RemoteData<Community>>;
}
}

View File

@@ -10,6 +10,10 @@ import { CommunityAuthorizationsComponent } from './community-authorizations/com
import { CommunityFormModule } from '../community-form/community-form.module';
import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module';
import { ComcolModule } from '../../shared/comcol/comcol.module';
import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component';
import {
AccessControlFormModule
} from '../../shared/access-control-form-container/access-control-form.module';
/**
* Module that contains all components related to the Edit Community page administrator functionality
@@ -22,13 +26,15 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
CommunityFormModule,
ComcolModule,
ResourcePoliciesModule,
AccessControlFormModule,
],
declarations: [
EditCommunityPageComponent,
CommunityCurateComponent,
CommunityMetadataComponent,
CommunityRolesComponent,
CommunityAuthorizationsComponent
CommunityAuthorizationsComponent,
CommunityAccessControlComponent
]
})
export class EditCommunityPageModule {

View File

@@ -11,6 +11,7 @@ import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/cr
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard';
import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component';
/**
* Routing module that handles the routing for the Edit Community page administrator functionality
@@ -51,6 +52,11 @@ import { CommunityAdministratorGuard } from '../../core/data/feature-authorizati
component: CommunityCurateComponent,
data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true }
},
{
path: 'access-control',
component: CommunityAccessControlComponent,
data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true }
},
/*{
path: 'authorizations',
component: CommunityAuthorizationsComponent,

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { ConfigDataService } from './config-data.service';
import { dataService } from '../data/base/data-service.decorator';
import { BULK_ACCESS_CONDITION_OPTIONS } from './models/config-type';
/**
* Data Service responsible for retrieving Bulk Access Condition Options from the REST API
*/
@Injectable({ providedIn: 'root' })
@dataService(BULK_ACCESS_CONDITION_OPTIONS)
export class BulkAccessConfigDataService extends ConfigDataService {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
) {
super('bulkaccessconditionoptions', requestService, rdbService, objectCache, halService);
}
}

View File

@@ -1,16 +1,17 @@
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ConfigObject } from './models/config.model';
import { RemoteData } from '../data/remote-data';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { map } from 'rxjs/operators';
import { BaseDataService } from '../data/base/base-data.service';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
/**
* Abstract data service to retrieve configuration objects from the REST server.
* Common logic for configuration objects should be implemented here.
*/
export abstract class ConfigDataService extends BaseDataService<ConfigObject> {
export abstract class ConfigDataService extends IdentifiableDataService<ConfigObject> {
/**
* Returns an observable of {@link RemoteData} of an object, based on an href, with a list of
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
@@ -37,4 +38,21 @@ export abstract class ConfigDataService extends BaseDataService<ConfigObject> {
}),
);
}
/**
* Returns a config object by given name
*
* Throws an error if a configuration object cannot be retrieved.
*
* @param name The name of configuration to retrieve
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
public findByName(name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ConfigObject>[]): Observable<RemoteData<ConfigObject>> {
return super.findById(name, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -0,0 +1,38 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { ResourceType } from '../../shared/resource-type';
import { HALLink } from '../../shared/hal-link.model';
import { ConfigObject } from './config.model';
import { AccessesConditionOption } from './config-accesses-conditions-options.model';
import { BULK_ACCESS_CONDITION_OPTIONS } from './config-type';
/**
* Model class for a bulk access condition options
*/
@typedObject
@inheritSerialization(ConfigObject)
export class BulkAccessConditionOptions extends ConfigObject {
static type = BULK_ACCESS_CONDITION_OPTIONS;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
@autoserializeAs(String, 'name')
uuid: string;
@autoserialize
id: string;
@autoserialize
itemAccessConditionOptions: AccessesConditionOption[];
@autoserialize
bitstreamAccessConditionOptions: AccessesConditionOption[];
_links: { self: HALLink };
}

View File

@@ -17,3 +17,5 @@ export const SUBMISSION_UPLOADS_TYPE = new ResourceType('submissionuploads');
export const SUBMISSION_UPLOAD_TYPE = new ResourceType('submissionupload');
export const SUBMISSION_ACCESSES_TYPE = new ResourceType('submissionaccessoption');
export const BULK_ACCESS_CONDITION_OPTIONS = new ResourceType('bulkaccessconditionoption');

View File

@@ -181,6 +181,7 @@ import { HierarchicalBrowseDefinition } from './shared/hierarchical-browse-defin
import { FlatBrowseDefinition } from './shared/flat-browse-definition.model';
import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -379,6 +380,7 @@ export const models =
IdentifierData,
Subscription,
ItemRequest,
BulkAccessConditionOptions
];
@NgModule({

View File

@@ -37,4 +37,6 @@ export enum Context {
MyDSpaceApproved = 'mydspaceApproved',
MyDSpaceWaitingController = 'mydspaceWaitingController',
MyDSpaceValidation = 'mydspaceValidation',
Bitstream = 'bitstream',
}

View File

@@ -14,14 +14,4 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
*/
export class JournalIssueSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnails if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
}

View File

@@ -14,14 +14,4 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
*/
export class JournalVolumeSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnails if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
}

View File

@@ -14,14 +14,4 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
*/
export class JournalSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnails if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
}

View File

@@ -14,14 +14,4 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
*/
export class OrgUnitSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnail if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
}

View File

@@ -14,14 +14,4 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
*/
export class ProjectSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnail if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
}

View File

@@ -41,6 +41,11 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module';
import { ItemCurateComponent } from './item-curate/item-curate.component';
import { ThemedItemStatusComponent } from './item-status/themed-item-status.component';
import { ItemAccessControlComponent } from './item-access-control/item-access-control.component';
import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module';
import {
AccessControlFormModule
} from '../../shared/access-control-form-container/access-control-form.module';
/**
* Module that contains all components related to the Edit Item page administrator functionality
@@ -57,6 +62,8 @@ import { ThemedItemStatusComponent } from './item-status/themed-item-status.comp
NgbModule,
ItemVersionsModule,
DsoSharedModule,
ResultsBackButtonModule,
AccessControlFormModule,
],
declarations: [
EditItemPageComponent,
@@ -85,7 +92,8 @@ import { ThemedItemStatusComponent } from './item-status/themed-item-status.comp
ItemAuthorizationsComponent,
IdentifierDataComponent,
ItemRegisterDoiComponent,
ItemCurateComponent
ItemCurateComponent,
ItemAccessControlComponent,
],
providers: [
BundleDataService,

View File

@@ -42,6 +42,7 @@ import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metada
import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard';
import { ItemCurateComponent } from './item-curate/item-curate.component';
import { ThemedItemStatusComponent } from './item-status/themed-item-status.component';
import { ItemAccessControlComponent } from './item-access-control/item-access-control.component';
/**
* Routing module that handles the routing for the Edit Item page administrator functionality
@@ -112,6 +113,11 @@ import { ThemedItemStatusComponent } from './item-status/themed-item-status.comp
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
canActivate: [ItemPageVersionHistoryGuard]
},
{
path: 'access-control',
component: ItemAccessControlComponent,
data: { title: 'item.edit.tabs.access-control.title', showBreadcrumbs: true }
},
{
path: 'mapper',
component: ItemCollectionMapperComponent,

View File

@@ -0,0 +1,6 @@
<ds-access-control-form-container
titleMessage="item-access-control-title"
*ngIf="itemRD$ | async as itemRD"
[itemRD]="itemRD"
[showLimitToSpecificBitstreams]="true">
</ds-access-control-form-container>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemAccessControlComponent } from './item-access-control.component';
xdescribe('ItemAccessControlComponent', () => {
let component: ItemAccessControlComponent;
let fixture: ComponentFixture<ItemAccessControlComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ItemAccessControlComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ItemAccessControlComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,26 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'ds-item-access-control',
templateUrl: './item-access-control.component.html',
styleUrls: [ './item-access-control.component.scss' ],
})
export class ItemAccessControlComponent implements OnInit {
itemRD$: Observable<RemoteData<Item>>;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.itemRD$ = this.route.parent.parent.data.pipe(
map((data) => data.dso)
).pipe(getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
}
}

View File

@@ -659,6 +659,17 @@ export class MenuResolver implements Resolve<boolean> {
link: '/access-control/groups'
} as LinkMenuItemModel,
},
{
id: 'access_control_bulk',
parentID: 'access_control',
active: false,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_bulk',
link: '/access-control/bulk-access'
} as LinkMenuItemModel,
},
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'access_control_authorizations',

View File

@@ -0,0 +1,111 @@
<form #ngForm="ngForm">
<div *ngIf="showWarning" class="alert alert-warning">
{{'access-control-no-access-conditions-warning-message' | translate}}
</div>
<ng-container *ngFor="let control of form.accessControls; trackBy: trackById">
<div ngModelGroup="access-control-item-{{control.id}}" class="access-control-item mt-3">
<div class="d-flex flex-column">
<div>
<label for="accesscontroloption-{{control.id}}">
{{'access-control-option-label' | translate}}
</label>
<select
id="accesscontroloption-{{control.id}}"
[disabled]="ngForm.disabled"
[(ngModel)]="control.itemName"
(ngModelChange)="accessControlChanged(control, $event)"
name="itemName-{{control.id}}"
class="form-control">
<option value=""></option>
<option *ngFor="let option of dropdownOptions" [value]="option.name">
{{ option.name }}
</option>
</select>
<small class="form-text text-muted">
{{'access-control-option-note' | translate}}
</small>
</div>
<div *ngIf="control.hasStartDate" class="mt-3">
<label for="accesscontrolstartdate-{{control.id}}">
{{'access-control-option-start-date' | translate}}
</label>
<div class="input-group">
<input
id="accesscontrolstartdate-{{control.id}}"
class="form-control"
placeholder="yyyy-mm-dd"
[(ngModel)]="control.startDate"
name="startDate-{{control.id}}"
[maxDate]="control.maxStartDate | toDate"
ngbDatepicker
placement="top-start top-end bottom-start bottom-end"
#d="ngbDatepicker"
/>
<div class="input-group-append">
<button
class="btn btn-outline-secondary fas fa-calendar"
[disabled]="ngForm.disabled"
(click)="d.toggle()" type="button">
</button>
</div>
</div>
<small class="form-text text-muted">
{{'access-control-option-start-date-note' | translate}}
</small>
</div>
<div *ngIf="control.hasEndDate" class="mt-3">
<label for="accesscontrolenddate-{{control.id}}">
{{'access-control-option-end-date' | translate}}
</label>
<div class="input-group">
<input
id="accesscontrolenddate-{{control.id}}"
class="form-control"
placeholder="yyyy-mm-dd"
[(ngModel)]="control.endDate"
name="endDate-{{control.id}}"
[maxDate]="control.maxEndDate | toDate"
ngbDatepicker
placement="top-start top-end bottom-start bottom-end"
#d1="ngbDatepicker"
/>
<div class="input-group-append">
<button
type="button" class="btn btn-outline-secondary fas fa-calendar"
[disabled]="ngForm.disabled"
(click)="d1.toggle()">
</button>
</div>
</div>
<small class="form-text text-muted">
{{'access-control-option-end-date-note' | translate}}
</small>
</div>
</div>
<div>
<label>&nbsp;</label>
<div class="input-group">
<button type="button" class="btn btn-outline-danger"
[disabled]="ngForm.disabled || form.accessControls.length === 1"
(click)="removeAccessControlItem(control.id)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</ng-container>
<button type="button" id="add-btn-{{type}}" class="btn btn-outline-primary mt-3"
[disabled]="ngForm.disabled"
(click)="addAccessControlItem()">
<i class="fas fa-plus"></i>
{{'access-control-add-more' | translate}}
</button>
</form>

View File

@@ -0,0 +1,7 @@
.access-control-item {
display: grid;
grid-template-columns: 1fr 50px;
grid-gap: 10px;
padding-bottom: 15px;
border-bottom: 2px dotted grey;
}

View File

@@ -0,0 +1,116 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccessControlArrayFormComponent } from './access-control-array-form.component';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ToDatePipe } from './to-date.pipe';
import { SharedBrowseByModule } from '../../browse-by/shared-browse-by.module';
describe('AccessControlArrayFormComponent', () => {
let component: AccessControlArrayFormComponent;
let fixture: ComponentFixture<AccessControlArrayFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ CommonModule, FormsModule, SharedBrowseByModule, TranslateModule.forRoot(), NgbDatepickerModule ],
declarations: [ AccessControlArrayFormComponent, ToDatePipe ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AccessControlArrayFormComponent);
component = fixture.componentInstance;
component.dropdownOptions = [{name: 'Option1'}, {name: 'Option2'}] as any;
component.type = 'item';
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have only one empty control access item in the form', () => {
const accessControlItems = fixture.debugElement.queryAll(By.css('.access-control-item'));
expect(accessControlItems.length).toEqual(1);
});
it('should add access control item', () => {
component.addAccessControlItem();
expect(component.form.accessControls.length).toEqual(2);
});
it('should remove access control item', () => {
expect(component.form.accessControls.length).toEqual(1);
component.addAccessControlItem();
expect(component.form.accessControls.length).toEqual(2);
const id = component.form.accessControls[0].id;
component.removeAccessControlItem(id);
expect(component.form.accessControls.length).toEqual(1);
});
it('should reset form value', () => {
const item = { itemName: 'item1', startDate: '2022-01-01', endDate: '2022-02-01' };
component.addAccessControlItem(item.itemName);
// set value to item1
component.accessControlChanged(
component.form.accessControls[1],
'item1'
);
component.reset();
expect(component.form.accessControls[1]?.itemName).toEqual(undefined);
});
it('should display a select dropdown with options', () => {
component.enable();
fixture.detectChanges();
const id = component.form.accessControls[0].id;
const selectElement: DebugElement = fixture.debugElement.query(By.css(`select#accesscontroloption-${id}`));
expect(selectElement).toBeTruthy();
const options = selectElement.nativeElement.querySelectorAll('option');
expect(options.length).toEqual(3); // 2 options + default empty option
expect(options[0].value).toEqual('');
expect(options[1].value).toEqual('Option1');
expect(options[2].value).toEqual('Option2');
});
it('should add new access control items when clicking "Add more" button', () => {
component.enable();
fixture.detectChanges();
const addButton: DebugElement = fixture.debugElement.query(By.css(`button#add-btn-${component.type}`));
addButton.nativeElement.click();
fixture.detectChanges();
const accessControlItems = fixture.debugElement.queryAll(By.css('.access-control-item'));
expect(accessControlItems.length).toEqual(2);
});
it('should remove access control items when clicking remove button', () => {
component.enable();
component.addAccessControlItem('test');
fixture.detectChanges();
const removeButton: DebugElement[] = fixture.debugElement.queryAll(By.css('button.btn-outline-danger'));
removeButton[1].nativeElement.click();
fixture.detectChanges();
const accessControlItems = fixture.debugElement.queryAll(By.css('.access-control-item'));
expect(accessControlItems.length).toEqual(1);
});
});

View File

@@ -0,0 +1,150 @@
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {NgForm} from '@angular/forms';
import {AccessesConditionOption} from '../../../core/config/models/config-accesses-conditions-options.model';
import {dateToISOFormat} from '../../date.util';
@Component({
selector: 'ds-access-control-array-form',
templateUrl: './access-control-array-form.component.html',
styleUrls: ['./access-control-array-form.component.scss'],
exportAs: 'accessControlArrayForm'
})
export class AccessControlArrayFormComponent implements OnInit {
@Input() dropdownOptions: AccessesConditionOption[] = [];
@Input() mode!: 'add' | 'replace';
@Input() type!: 'item' | 'bitstream';
@ViewChild('ngForm', {static: true}) ngForm!: NgForm;
form: { accessControls: AccessControlItem[] } = {
accessControls: [emptyAccessControlItem()] // Start with one empty access control item
};
formDisabled = true;
ngOnInit(): void {
this.disable(); // Disable the form by default
}
get allControlsAreEmpty() {
return this.form.accessControls
.every(x => x.itemName === null || x.itemName === '');
}
get showWarning() {
return this.mode === 'replace' && this.allControlsAreEmpty && !this.formDisabled;
}
/**
* Add a new access control item to the form.
* Start and end date are disabled by default.
* @param itemName The name of the item to add
*/
addAccessControlItem(itemName: string = null) {
this.form.accessControls = [
...this.form.accessControls,
{...emptyAccessControlItem(), itemName}
];
}
/**
* Remove an access control item from the form.
* @param ngModelGroup
* @param index
*/
removeAccessControlItem(id: number) {
this.form.accessControls = this.form.accessControls.filter(item => item.id !== id);
}
/**
* Get the value of the form.
* This will be used to read the form value from the parent component.
* @return The form value
*/
getValue() {
return this.form.accessControls
.filter(x => x.itemName !== null && x.itemName !== '')
.map(x => ({
name: x.itemName,
startDate: (x.startDate ? dateToISOFormat(x.startDate) : null),
endDate: (x.endDate ? dateToISOFormat(x.endDate) : null)
}));
}
/**
* Set the value of the form from the parent component.
*/
reset() {
this.form.accessControls = [];
// Add an empty access control item by default
this.addAccessControlItem();
this.disable();
}
/**
* Disable the form.
* This will be used to disable the form from the parent component.
*/
disable = () => {
this.ngForm.form.disable();
this.formDisabled = true;
};
/**
* Enable the form.
* This will be used to enable the form from the parent component.
*/
enable = () => {
this.ngForm.form.enable();
this.formDisabled = false;
};
accessControlChanged(control: AccessControlItem, selectedItem: string) {
const item = this.dropdownOptions
.find((x) => x.name === selectedItem);
control.startDate = null;
control.endDate = null;
control.hasStartDate = item?.hasStartDate || false;
control.hasEndDate = item?.hasEndDate || false;
control.maxStartDate = item?.maxStartDate || null;
control.maxEndDate = item?.maxEndDate || null;
}
trackById(index: number, item: AccessControlItem) {
return item.id;
}
}
export interface AccessControlItem {
id: number; // will be used only locally
itemName: string | null;
hasStartDate?: boolean;
startDate: string | null;
maxStartDate?: string | null;
hasEndDate?: boolean;
endDate: string | null;
maxEndDate?: string | null;
}
const emptyAccessControlItem = (): AccessControlItem => ({
id: randomID(),
itemName: null,
startDate: null,
hasStartDate: false,
maxStartDate: null,
endDate: null,
hasEndDate: false,
maxEndDate: null,
});
const randomID = () => Math.floor(Math.random() * 1000000);

View File

@@ -0,0 +1,23 @@
import {Pipe, PipeTransform} from '@angular/core';
import {NgbDateStruct} from '@ng-bootstrap/ng-bootstrap/datepicker/ngb-date-struct';
@Pipe({
// eslint-disable-next-line @angular-eslint/pipe-prefix
name: 'toDate',
pure: false
})
export class ToDatePipe implements PipeTransform {
transform(dateValue: string | null): NgbDateStruct | null {
if (!dateValue) {
return null;
}
const date = new Date(dateValue);
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate()
} as NgbDateStruct;
}
}

View File

@@ -0,0 +1,27 @@
import {ListableObject} from '../object-collection/shared/listable-object.model';
export const createAccessControlInitialFormState = (): AccessControlFormState => ({
item: {
toggleStatus: false,
accessMode: 'replace',
},
bitstream: {
toggleStatus: false,
accessMode: 'replace',
changesLimit: 'all', // 'all' | 'selected'
selectedBitstreams: [] as ListableObject[],
},
});
export interface AccessControlFormState {
item: {
toggleStatus: boolean,
accessMode: 'add' | 'replace',
},
bitstream: {
toggleStatus: boolean,
accessMode: 'add' | 'replace',
changesLimit: string,
selectedBitstreams: ListableObject[],
}
}

View File

@@ -0,0 +1,167 @@
<div class="container">
<div class="card border-0">
<div class="card-body">
<ds-alert
*ngIf="titleMessage"
[type]="AlertType.Info"
[content]="titleMessage">
</ds-alert>
<div class="row mt-5">
<div class="col-12 col-md-6 border-right d-flex flex-column justify-content-between">
<div>
<div class="d-flex align-items-center">
<h4 class="mb-0 mr-4">
{{ 'access-control-item-header-toggle' | translate }}
</h4>
<ui-switch
[(ngModel)]="state.item.toggleStatus"
(ngModelChange)="handleStatusChange('item', $event)">
</ui-switch>
</div>
<div class="row mt-3">
<div class="col-12 col-md-3">
{{ 'access-control-mode' | translate }}
</div>
<div class="col-12 col-md-8">
<div class="form-check">
<input class="form-check-input" type="radio"
name="itemMode" id="itemReplace" value="replace"
[disabled]="!state.item.toggleStatus"
[(ngModel)]="state.item.accessMode">
<label class="form-check-label" for="itemReplace">
{{'access-control-replace-all' | translate}}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio"
name="itemMode" id="itemAdd" value="add"
[disabled]="!state.item.toggleStatus"
[(ngModel)]="state.item.accessMode">
<label class="form-check-label" for="itemAdd">
{{'access-control-add-to-existing' | translate}}
</label>
</div>
</div>
</div>
</div>
<div>
<h5 class="mt-3">{{'access-control-access-conditions' | translate}}</h5>
<ds-access-control-array-form
#itemAccessCmp
[type]="'item'"
[mode]="state.item.accessMode"
[dropdownOptions]="(dropdownData$ | async)?.itemAccessConditionOptions || []">
</ds-access-control-array-form>
</div>
</div>
<div class="col-12 col-md-6 d-flex flex-column justify-content-between">
<div>
<div class="d-flex align-items-center">
<h4 class="mb-0 mr-4">
{{'access-control-bitstream-header-toggle' | translate}}
</h4>
<ui-switch
[(ngModel)]="state.bitstream.toggleStatus"
(ngModelChange)="handleStatusChange('bitstream', $event)">
</ui-switch>
</div>
<div *ngIf="showLimitToSpecificBitstreams" class="row mt-3">
<div class="col-12">
{{'access-control-limit-to-specific' | translate}}
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="radio"
name="changesLimit" id="processAll" value="all"
[disabled]="!state.bitstream.toggleStatus"
[(ngModel)]="state.bitstream.changesLimit">
<label class="form-check-label" for="processAll">
{{'access-control-process-all-bitstreams' | translate}}
</label>
</div>
<div class="form-check">
<input class="form-check-input mt-2" type="radio"
name="changesLimit" id="processSelected" value="selected"
[disabled]="!state.bitstream.toggleStatus"
[(ngModel)]="state.bitstream.changesLimit">
<label class="form-check-label" for="processSelected">
{{ state.bitstream.selectedBitstreams.length }}
{{'access-control-bitstreams-selected' | translate}}
<button
*ngIf="itemRD"
[disabled]="!state.bitstream.toggleStatus || state.bitstream.changesLimit !== 'selected'"
(click)="openSelectBitstreamsModal(itemRD.payload)"
class="btn btn-outline-dark border-0" type="button">
<i class="fa fa-search"></i>
</button>
</label>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-md-3">
{{'access-control-mode' | translate}}
</div>
<div class="col-12 col-md-8">
<div class="form-check">
<input class="form-check-input" type="radio"
name="bitstreamMode" id="bitstreamReplace" value="replace"
[disabled]="!state.bitstream.toggleStatus"
[(ngModel)]="state.bitstream.accessMode">
<label class="form-check-label" for="bitstreamReplace">
{{'access-control-replace-all' | translate}}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio"
name="bitstreamMode" id="bitstreamAdd" value="add"
[disabled]="!state.bitstream.toggleStatus"
[(ngModel)]="state.bitstream.accessMode">
<label class="form-check-label" for="bitstreamAdd">
{{'access-control-add-to-existing' | translate}}
</label>
</div>
</div>
</div>
<div>
<h5 class="mt-3">{{'access-control-access-conditions' | translate}}</h5>
<ds-access-control-array-form
#bitstreamAccessCmp
[type]="'bitstream'"
[mode]="state.bitstream.accessMode"
[dropdownOptions]="(dropdownData$ | async)?.bitstreamAccessConditionOptions || []">
</ds-access-control-array-form>
</div>
</div>
</div>
<hr *ngIf="showSubmit">
<div *ngIf="showSubmit" class="d-flex justify-content-end">
<button class="btn btn-outline-primary mr-3" (click)="reset()" type="button">
{{ 'access-control-cancel' | translate }}
</button>
<button class="btn btn-primary"
[disabled]="!state.item.toggleStatus && !state.bitstream.toggleStatus"
(click)="submit()" type="submit">
{{ 'access-control-execute' | translate }}
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,149 @@
import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
import {NgbDatepickerModule, NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
import {Component} from '@angular/core';
import {of} from 'rxjs';
import {AccessControlFormContainerComponent} from './access-control-form-container.component';
import {BulkAccessControlService} from './bulk-access-control.service';
import {BulkAccessConfigDataService} from '../../core/config/bulk-access-config-data.service';
import {Item} from '../../core/shared/item.model';
import {SelectableListService} from '../object-list/selectable-list/selectable-list.service';
import {createAccessControlInitialFormState} from './access-control-form-container-intial-state';
import {CommonModule} from '@angular/common';
import {SharedBrowseByModule} from '../browse-by/shared-browse-by.module';
import {TranslateModule} from '@ngx-translate/core';
import {FormsModule} from '@angular/forms';
import {UiSwitchModule} from 'ngx-ui-switch';
import {
ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID
} from './item-access-control-select-bitstreams-modal/item-access-control-select-bitstreams-modal.component';
import {AccessControlFormModule} from './access-control-form.module';
describe('AccessControlFormContainerComponent', () => {
let component: AccessControlFormContainerComponent<any>;
let fixture: ComponentFixture<AccessControlFormContainerComponent<any>>;
// Mock NgbModal
@Component({selector: 'ds-ngb-modal', template: ''})
class MockNgbModalComponent {
}
// Mock dependencies
const mockBulkAccessControlService = {
createPayloadFile: jasmine.createSpy('createPayloadFile').and.returnValue({file: 'mocked-file'}),
executeScript: jasmine.createSpy('executeScript').and.returnValue(of('success')),
};
const mockBulkAccessConfigDataService = {
findByName: jasmine.createSpy('findByName').and.returnValue(of({payload: {options: []}})),
};
const mockSelectableListService = {
getSelectableList: jasmine.createSpy('getSelectableList').and.returnValue(of({selection: []})),
deselectAll: jasmine.createSpy('deselectAll'),
};
const mockNgbModal = {
open: jasmine.createSpy('open').and.returnValue(
{ componentInstance: {}, closed: of({})} as NgbModalRef
)
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AccessControlFormContainerComponent, MockNgbModalComponent],
imports: [
CommonModule,
FormsModule,
SharedBrowseByModule,
AccessControlFormModule,
TranslateModule.forRoot(),
NgbDatepickerModule,
UiSwitchModule
],
providers: [
{provide: BulkAccessControlService, useValue: mockBulkAccessControlService},
{provide: BulkAccessConfigDataService, useValue: mockBulkAccessConfigDataService},
{provide: SelectableListService, useValue: mockSelectableListService},
{provide: NgbModal, useValue: mockNgbModal},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AccessControlFormContainerComponent);
component = fixture.componentInstance;
component.state = createAccessControlInitialFormState();
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should reset the form', fakeAsync(() => {
fixture.detectChanges();
const resetSpy = spyOn(component.bitstreamAccessCmp, 'reset');
spyOn(component.itemAccessCmp, 'reset');
component.reset();
expect(resetSpy).toHaveBeenCalled();
expect(component.itemAccessCmp.reset).toHaveBeenCalled();
expect(component.state).toEqual(createAccessControlInitialFormState());
}));
it('should submit the form', () => {
const bitstreamAccess = 'bitstreamAccess';
const itemAccess = 'itemAccess';
component.bitstreamAccessCmp.getValue = jasmine.createSpy('getValue').and.returnValue(bitstreamAccess);
component.itemAccessCmp.getValue = jasmine.createSpy('getValue').and.returnValue(itemAccess);
component.itemRD = {payload: {uuid: 'item-uuid'}} as any;
component.submit();
expect(mockBulkAccessControlService.createPayloadFile).toHaveBeenCalledWith({
bitstreamAccess,
itemAccess,
state: createAccessControlInitialFormState(),
});
expect(mockBulkAccessControlService.executeScript).toHaveBeenCalledWith(['item-uuid'], 'mocked-file');
});
it('should handle the status change for bitstream access', () => {
component.bitstreamAccessCmp.enable = jasmine.createSpy('enable');
component.bitstreamAccessCmp.disable = jasmine.createSpy('disable');
component.handleStatusChange('bitstream', true);
expect(component.bitstreamAccessCmp.enable).toHaveBeenCalled();
component.handleStatusChange('bitstream', false);
expect(component.bitstreamAccessCmp.disable).toHaveBeenCalled();
});
it('should handle the status change for item access', () => {
component.itemAccessCmp.enable = jasmine.createSpy('enable');
component.itemAccessCmp.disable = jasmine.createSpy('disable');
component.handleStatusChange('item', true);
expect(component.itemAccessCmp.enable).toHaveBeenCalled();
component.handleStatusChange('item', false);
expect(component.itemAccessCmp.disable).toHaveBeenCalled();
});
it('should open the select bitstreams modal', () => {
const modalService = TestBed.inject(NgbModal);
component.openSelectBitstreamsModal(new Item());
expect(modalService.open).toHaveBeenCalled();
});
it('should unsubscribe and deselect all on component destroy', () => {
component.ngOnDestroy();
expect(component.selectableListService.deselectAll).toHaveBeenCalledWith(
ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID
);
});
});

View File

@@ -0,0 +1,160 @@
import { ChangeDetectorRef, Component, Input, OnDestroy, ViewChild } from '@angular/core';
import { concatMap, Observable, shareReplay } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model';
import { AccessControlArrayFormComponent } from './access-control-array-form/access-control-array-form.component';
import { BulkAccessControlService } from './bulk-access-control.service';
import { SelectableListService } from '../object-list/selectable-list/selectable-list.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { map, take } from 'rxjs/operators';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import {
ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID,
ItemAccessControlSelectBitstreamsModalComponent
} from './item-access-control-select-bitstreams-modal/item-access-control-select-bitstreams-modal.component';
import { BulkAccessConfigDataService } from '../../core/config/bulk-access-config-data.service';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { BulkAccessConditionOptions } from '../../core/config/models/bulk-access-condition-options.model';
import { AlertType } from '../alert/aletr-type';
import {
createAccessControlInitialFormState
} from './access-control-form-container-intial-state';
@Component({
selector: 'ds-access-control-form-container',
templateUrl: './access-control-form-container.component.html',
styleUrls: [ './access-control-form-container.component.scss' ],
exportAs: 'dsAccessControlForm'
})
export class AccessControlFormContainerComponent<T extends DSpaceObject> implements OnDestroy {
/**
* Will be used to determine if we need to show the limit changes to specific bitstreams radio buttons
*/
@Input() showLimitToSpecificBitstreams = false;
/**
* The title message of the access control form (translate key)
*/
@Input() titleMessage = '';
/**
* The item to which the access control form applies
*/
@Input() itemRD: RemoteData<T>;
/**
* Whether to show the submit and cancel button
* We want to hide these buttons when the form is
* used in an accordion, and we want to show buttons somewhere else
*/
@Input() showSubmit = true;
@ViewChild('bitstreamAccessCmp', { static: true }) bitstreamAccessCmp: AccessControlArrayFormComponent;
@ViewChild('itemAccessCmp', { static: true }) itemAccessCmp: AccessControlArrayFormComponent;
readonly AlertType = AlertType;
constructor(
private bulkAccessConfigService: BulkAccessConfigDataService,
private bulkAccessControlService: BulkAccessControlService,
public selectableListService: SelectableListService,
protected modalService: NgbModal,
private cdr: ChangeDetectorRef
) {}
state = createAccessControlInitialFormState();
dropdownData$: Observable<BulkAccessConditionOptions> = this.bulkAccessConfigService.findByName('default').pipe(
getFirstCompletedRemoteData(),
map((configRD: RemoteData<BulkAccessConditionOptions>) => configRD.hasSucceeded ? configRD.payload : null),
shareReplay(1)
);
/**
* 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(),
state: this.state
};
}
/**
* Reset the form to its initial state
* This will also reset the state of the child components (bitstream and item access)
*/
reset() {
this.bitstreamAccessCmp.reset();
this.itemAccessCmp.reset();
this.state = createAccessControlInitialFormState();
}
/**
* Submit the form
* This will create a payload file and execute the script
*/
submit() {
const bitstreamAccess = this.bitstreamAccessCmp.getValue();
const itemAccess = this.itemAccessCmp.getValue();
const { file } = this.bulkAccessControlService.createPayloadFile({
bitstreamAccess,
itemAccess,
state: this.state
});
this.bulkAccessControlService.executeScript(
[ this.itemRD.payload.uuid ],
file
).pipe(take(1)).subscribe((res) => {
console.log('success', res);
});
}
/**
* Handle the status change of the access control form (item or bitstream)
* This will enable/disable the access control form for the item or bitstream
* @param type The type of the access control form (item or bitstream)
* @param active boolean indicating whether the access control form should be enabled or disabled
*/
handleStatusChange(type: 'item' | 'bitstream', active: boolean) {
if (type === 'bitstream') {
active ? this.bitstreamAccessCmp.enable() : this.bitstreamAccessCmp.disable();
} else if (type === 'item') {
active ? this.itemAccessCmp.enable() : this.itemAccessCmp.disable();
}
}
/**
* Open the modal to select bitstreams for which to change the access control
* This will open the modal and pass the currently selected bitstreams
* @param item The item for which to change the access control
*/
openSelectBitstreamsModal(item: Item) {
const ref = this.modalService.open(ItemAccessControlSelectBitstreamsModalComponent);
ref.componentInstance.selectedBitstreams = this.state.bitstream.selectedBitstreams;
ref.componentInstance.item = item;
ref.closed.pipe(
concatMap(() => this.selectableListService.getSelectableList(ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID)),
take(1)
).subscribe((list) => {
this.state.bitstream.selectedBitstreams = list?.selection || [];
this.cdr.detectChanges();
});
}
ngOnDestroy(): void {
this.selectableListService.deselectAll(ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID);
}
}

View File

@@ -0,0 +1,32 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {TranslateModule} from '@ngx-translate/core';
import {UiSwitchModule} from 'ngx-ui-switch';
import {AccessControlArrayFormComponent} from './access-control-array-form/access-control-array-form.component';
import {SharedModule} from '../shared.module';
import {
ItemAccessControlSelectBitstreamsModalComponent
} from './item-access-control-select-bitstreams-modal/item-access-control-select-bitstreams-modal.component';
import {AccessControlFormContainerComponent} from './access-control-form-container.component';
import {NgbDatepickerModule} from '@ng-bootstrap/ng-bootstrap';
import {ToDatePipe} from './access-control-array-form/to-date.pipe';
@NgModule({
imports: [
CommonModule,
SharedModule,
TranslateModule,
UiSwitchModule,
NgbDatepickerModule
],
declarations: [
AccessControlFormContainerComponent,
AccessControlArrayFormComponent,
ItemAccessControlSelectBitstreamsModalComponent,
ToDatePipe
],
exports: [ AccessControlFormContainerComponent, AccessControlArrayFormComponent ],
})
export class AccessControlFormModule {}

View File

@@ -0,0 +1,94 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { BulkAccessControlService } from './bulk-access-control.service';
import { ScriptDataService } from '../../core/data/processes/script-data.service';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { NotificationsService } from '../notifications/notifications.service';
import { NotificationsServiceStub } from '../testing/notifications-service.stub';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { Process } from '../../process-page/processes/process.model';
describe('BulkAccessControlService', () => {
let service: BulkAccessControlService;
let scriptServiceSpy: jasmine.SpyObj<ScriptDataService>;
const mockPayload: any = {
'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': []
}
}
};
beforeEach(() => {
const spy = jasmine.createSpyObj('ScriptDataService', ['invoke']);
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot()
],
providers: [
BulkAccessControlService,
{ provide: ScriptDataService, useValue: spy },
{ provide: NotificationsService, useValue: NotificationsServiceStub },
]
});
service = TestBed.inject(BulkAccessControlService);
scriptServiceSpy = TestBed.inject(ScriptDataService) as jasmine.SpyObj<ScriptDataService>;
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('createPayloadFile', () => {
it('should create a file and return the URL and file object', () => {
const payload = mockPayload;
const result = service.createPayloadFile(payload);
expect(result.url).toBeTruthy();
expect(result.file).toBeTruthy();
});
});
describe('executeScript', () => {
it('should invoke the script service with the correct parameters', () => {
const uuids = ['123', '456'];
const file = new File(['test'], 'data.json', { type: 'application/json' });
const expectedParams: ProcessParameter[] = [
{ name: '-f', value: 'data.json' },
{ name: '-u', value: '123' },
{ name: '-u', value: '456' },
];
// @ts-ignore
scriptServiceSpy.invoke.and.returnValue(createSuccessfulRemoteDataObject$(new Process()));
const result = service.executeScript(uuids, file);
expect(scriptServiceSpy.invoke).toHaveBeenCalledWith('bulk-access-control', expectedParams, [file]);
expect(result).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,146 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
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-intial-state';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { RemoteData } from '../../core/data/remote-data';
import { Process } from '../../process-page/processes/process.model';
import { isNotEmpty } from '../empty.util';
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
import { NotificationsService } from '../notifications/notifications.service';
export interface BulkAccessPayload {
state: AccessControlFormState;
bitstreamAccess: any;
itemAccess: any;
}
/**
* This service is used to create a payload file and execute the bulk access control script
*/
@Injectable({ providedIn: 'root' })
export class BulkAccessControlService {
constructor(
private notificationsService: NotificationsService,
private router: Router,
private scriptService: ScriptDataService,
private translationService: TranslateService
) {}
/**
* Create a payload file from the given payload and return the file and the url to the file
* The created file will be used as input for the bulk access control script
* @param payload The payload to create the file from
*/
createPayloadFile(payload: BulkAccessPayload) {
const content = convertToBulkAccessControlFileModel(payload);
const blob = new Blob([JSON.stringify(content, null, 2)], {
type: 'application/json',
});
const file = new File([blob], 'data.json', {
type: 'application/json',
});
const url = URL.createObjectURL(file);
return { url, file };
}
/**
* Execute the bulk access control script with the given uuids and file
* @param uuids
* @param file
*/
executeScript(uuids: string[], file: File): Observable<boolean> {
console.log('execute', { uuids, file });
const params: ProcessParameter[] = [
{ name: '-f', value: file.name }
];
uuids.forEach((uuid) => {
params.push({ name: '-u', value: uuid });
});
return this.scriptService.invoke('bulk-access-control', params, [file]).pipe(
getFirstCompletedRemoteData(),
map((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
const title = this.translationService.get('process.new.notification.success.title');
const content = this.translationService.get('process.new.notification.success.content');
this.notificationsService.success(title, content);
if (isNotEmpty(rd.payload)) {
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
}
return true;
} else {
const title = this.translationService.get('process.new.notification.error.title');
const content = this.translationService.get('process.new.notification.error.content');
this.notificationsService.error(title, content);
return false;
}
})
);
}
}
/**
* Convert the given payload to a BulkAccessControlFileModel
* @param payload
*/
export const convertToBulkAccessControlFileModel = (payload: { state: AccessControlFormState, bitstreamAccess: AccessCondition[], itemAccess: AccessCondition[] }): BulkAccessControlFileModel => {
let finalPayload: BulkAccessControlFileModel = {};
const itemEnabled = payload.state.item.toggleStatus;
const bitstreamEnabled = payload.state.bitstream.toggleStatus;
if (itemEnabled) {
finalPayload.item = {
mode: payload.state.item.accessMode,
accessConditions: payload.itemAccess
};
}
if (bitstreamEnabled) {
const constraints = { uuid: [] };
if (bitstreamEnabled && payload.state.bitstream.changesLimit === 'selected') {
// @ts-ignore
constraints.uuid = payload.state.bitstream.selectedBitstreams.map((x) => x.id);
}
finalPayload.bitstream = {
constraints,
mode: payload.state.bitstream.accessMode,
accessConditions: payload.bitstreamAccess
};
}
return finalPayload;
};
export interface BulkAccessControlFileModel {
item?: {
mode: string;
accessConditions: AccessCondition[];
},
bitstream?: {
constraints: { uuid: string[] };
mode: string;
accessConditions: AccessCondition[];
}
}
interface AccessCondition {
name: string;
startDate?: string;
endDate?: string;
}

View File

@@ -0,0 +1,35 @@
<div class="modal-header">
<h4 class="modal-title">
{{'access-control-select-bitstreams-modal.title' | translate}}
</h4>
<button type="button" class="close" aria-label="Close"
(click)="activeModal.dismiss('Cross click')">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ng-container *ngIf="data$ | async as data">
<ds-viewable-collection
*ngIf="data.payload.page.length > 0"
[config]="paginationConfig"
[context]="context"
[objects]="data"
[selectable]="true"
[selectionConfig]="{ repeatable: true, listId: LIST_ID }"
[showPaginator]="true"
(pageChange)="loadForPage($event)">
</ds-viewable-collection>
<div *ngIf="data && data.payload.page.length === 0"
class="alert alert-info w-100" role="alert">
{{'access-control-select-bitstreams-modal.no-items' | translate}}
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark"
(click)="activeModal.close('Close click')">
{{'access-control-select-bitstreams-modal.close' | translate}}
</button>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemAccessControlSelectBitstreamsModalComponent } from './item-access-control-select-bitstreams-modal.component';
xdescribe('ItemAccessControlSelectBitstreamsModalComponent', () => {
let component: ItemAccessControlSelectBitstreamsModalComponent;
let fixture: ComponentFixture<ItemAccessControlSelectBitstreamsModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ItemAccessControlSelectBitstreamsModalComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ItemAccessControlSelectBitstreamsModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,62 @@
import { Component, Input, OnInit } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject } from 'rxjs';
import { PaginatedList } from 'src/app/core/data/paginated-list.model';
import { RemoteData } from 'src/app/core/data/remote-data';
import { Bitstream } from 'src/app/core/shared/bitstream.model';
import { Context } from 'src/app/core/shared/context.model';
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
import { Item } from '../../../core/shared/item.model';
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { TranslateService } from '@ngx-translate/core';
import { hasValue } from '../../empty.util';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
export const ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID = 'item-access-control-select-bitstreams';
@Component({
selector: 'ds-item-access-control-select-bitstreams-modal',
templateUrl: './item-access-control-select-bitstreams-modal.component.html',
styleUrls: [ './item-access-control-select-bitstreams-modal.component.scss' ]
})
export class ItemAccessControlSelectBitstreamsModalComponent implements OnInit {
LIST_ID = ITEM_ACCESS_CONTROL_SELECT_BITSTREAMS_LIST_ID;
@Input() item!: Item;
@Input() selectedBitstreams: string[] = [];
data$ = new BehaviorSubject<RemoteData<PaginatedList<Bitstream>> | null>(null);
paginationConfig: PaginationComponentOptions;
pageSize = 5;
context: Context = Context.Bitstream;
constructor(
private bitstreamService: BitstreamDataService,
protected paginationService: PaginationService,
protected translateService: TranslateService,
public activeModal: NgbActiveModal
) { }
ngOnInit() {
this.loadForPage(1);
this.paginationConfig = new PaginationComponentOptions();
this.paginationConfig.id = 'iacsbm';
this.paginationConfig.currentPage = 1;
if (hasValue(this.pageSize)) {
this.paginationConfig.pageSize = this.pageSize;
}
}
loadForPage(page: number) {
this.bitstreamService.findAllByItemAndBundleName(this.item, 'ORIGINAL', { currentPage: page}, false)
.pipe(
getFirstCompletedRemoteData(),
)
.subscribe(this.data$);
}
}

View File

@@ -8,6 +8,7 @@
[context]="context"
[hidePaginationDetail]="hidePaginationDetail"
[showPaginator]="showPaginator"
[showThumbnails]="showThumbnails"
(paginationChange)="onPaginationChange($event)"
(pageChange)="onPageChange($event)"
(pageSizeChange)="onPageSizeChange($event)"
@@ -34,6 +35,7 @@
[context]="context"
[hidePaginationDetail]="hidePaginationDetail"
[showPaginator]="showPaginator"
[showThumbnails]="showThumbnails"
(paginationChange)="onPaginationChange($event)"
(pageChange)="onPageChange($event)"
(pageSizeChange)="onPageSizeChange($event)"
@@ -50,6 +52,7 @@
[context]="context"
[hidePaginationDetail]="hidePaginationDetail"
[showPaginator]="showPaginator"
[showThumbnails]="showThumbnails"
(contentChange)="contentChange.emit($event)"
*ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement">
</ds-object-detail>

View File

@@ -107,6 +107,11 @@ export class ObjectCollectionComponent implements OnInit {
*/
@Input() showPaginator = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* the page info of the list
*/

View File

@@ -70,6 +70,11 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
*/
@Input() showLabel = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* The value to display for this element
*/
@@ -105,6 +110,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
'linkType',
'listID',
'showLabel',
'showThumbnails',
'context',
'viewMode',
'value',

View File

@@ -42,6 +42,11 @@ export class AbstractListableElementComponent<T extends ListableObject> {
*/
@Input() showLabel = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* The context we matched on to get this component
*/

View File

@@ -21,6 +21,7 @@
<ds-listable-object-component-loader [object]="object"
[viewMode]="viewMode"
[context]="context"
[showThumbnails]="showThumbnails"
(contentChange)="contentChange.emit($event)"></ds-listable-object-component-loader>
</div>
</div>

View File

@@ -64,6 +64,11 @@ export class ObjectDetailComponent {
*/
@Input() showPaginator = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* Emit when one of the listed object has changed.
*/

View File

@@ -19,7 +19,11 @@
<div class="card-columns row" *ngIf="objects?.hasSucceeded">
<div class="card-column col col-sm-6 col-lg-4" *ngFor="let column of (columns$ | async)" @fadeIn>
<div class="card-element" *ngFor="let object of column" [attr.data-test]="'grid-object' | dsBrowserOnly">
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context" [linkType]="linkType"></ds-listable-object-component-loader>
<ds-listable-object-component-loader [object]="object"
[viewMode]="viewMode"
[context]="context"
[showThumbnails]="showThumbnails"
[linkType]="linkType"></ds-listable-object-component-loader>
</div>
</div>
</div>

View File

@@ -1,11 +1,12 @@
import { combineLatest as observableCombineLatest, BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { startWith, distinctUntilChanged, map } from 'rxjs/operators';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input, OnInit,
Input,
OnInit,
Output,
ViewEncapsulation
} from '@angular/core';
@@ -54,6 +55,11 @@ export class ObjectGridComponent implements OnInit {
*/
@Input() showPaginator = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* The whether or not the gear is hidden
*/

View File

@@ -0,0 +1,35 @@
import {CommonModule} from '@angular/common';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {TranslateModule} from '@ngx-translate/core';
import {SharedModule} from '../../shared.module';
import {BitstreamListItemComponent} from './bitstream-list-item.component';
import {DSONameService} from '../../../core/breadcrumbs/dso-name.service';
import {DSONameServiceMock} from '../../mocks/dso-name.service.mock';
describe('BitstreamListItemComponent', () => {
let component: BitstreamListItemComponent;
let fixture: ComponentFixture<BitstreamListItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BitstreamListItemComponent ],
imports: [ CommonModule, SharedModule, TranslateModule, RouterTestingModule ],
providers: [{ provide: DSONameService, useValue: new DSONameServiceMock() }]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BitstreamListItemComponent);
component = fixture.componentInstance;
// @ts-ignore
component.object = {name: 'test'};
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
import { ViewMode } from '../../../core/shared/view-mode.model';
import {
AbstractListableElementComponent
} from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { Context } from '../../../core/shared/context.model';
@listableObjectComponent(Bitstream, ViewMode.ListElement, Context.Bitstream)
@Component({
selector: 'ds-bitstream-list-item',
template: ` {{object.name}} `,
styleUrls: ['./bitstream-list-item.component.scss']
})
export class BitstreamListItemComponent extends AbstractListableElementComponent<Bitstream>{}

View File

@@ -81,6 +81,11 @@ export class ObjectListComponent {
*/
@Input() showPaginator = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* Emit when one of the listed object has changed.
*/

View File

@@ -23,7 +23,7 @@ export class CollectionSearchResultListElementComponent extends SearchResultList
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
this.showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails;
}
}

View File

@@ -23,6 +23,6 @@ export class CommunitySearchResultListElementComponent extends SearchResultListE
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
this.showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails;
}
}

View File

@@ -22,14 +22,9 @@ export class ItemSearchResultListElementComponent extends SearchResultListElemen
*/
itemPageRoute: string;
/**
* Display thumbnails if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
this.showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails;
this.itemPageRoute = getItemPageRoute(this.dso);
}
}

View File

@@ -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();
}

View File

@@ -1,13 +1,13 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ObjectListComponent } from './object-list.component';
import { ThemedComponent } from '../theme-support/themed.component';
import {PaginationComponentOptions} from '../pagination/pagination-component-options.model';
import {SortDirection, SortOptions} from '../../core/cache/models/sort-options.model';
import {CollectionElementLinkType} from '../object-collection/collection-element-link.type';
import {Context} from '../../core/shared/context.model';
import {RemoteData} from '../../core/data/remote-data';
import {PaginatedList} from '../../core/data/paginated-list.model';
import {ListableObject} from '../object-collection/shared/listable-object.model';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
import { Context } from '../../core/shared/context.model';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { ListableObject } from '../object-collection/shared/listable-object.model';
/**
* Themed wrapper for ObjectListComponent
@@ -78,6 +78,11 @@ export class ThemedObjectListComponent extends ThemedComponent<ObjectListCompone
*/
@Input() showPaginator: boolean;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* Emit when one of the listed object has changed.
*/
@@ -156,6 +161,7 @@ export class ThemedObjectListComponent extends ThemedComponent<ObjectListCompone
'importable',
'importConfig',
'showPaginator',
'showThumbnails',
'contentChange',
'prev',
'next',

View File

@@ -39,12 +39,12 @@
<div *ngIf="!showPaginator" class="d-flex justify-content-between">
<button id="nav-prev" type="button" class="btn btn-outline-primary float-left"
(click)="goPrev()"
[disabled]="objects?.payload?.currentPage <= 1">
[disabled]="(objects?.payload?.currentPage <= 1) && (paginationOptions?.currentPage <= 1)">
<i class="fas fa-angle-left"></i> {{'pagination.previous.button' |translate}}
</button>
<button id="nav-next" type="button" class="btn btn-outline-primary float-right"
(click)="goNext()"
[disabled]="objects?.payload?.currentPage >= objects?.payload?.totalPages">
[disabled]="(objects?.payload?.currentPage >= objects?.payload?.totalPages) || (paginationOptions?.currentPage >= objects?.payload?.totalPages)">
<span [ngbTooltip]="objects?.payload?.currentPage >= objects?.payload?.totalPages ? ('pagination.next.button.disabled.tooltip' |translate) : null">
<i class="fas fa-angle-right"></i> {{'pagination.next.button' |translate}}
</span>

View File

@@ -13,6 +13,7 @@
[linkType]="linkType"
[context]="context"
[hidePaginationDetail]="hidePaginationDetail"
[showThumbnails]="showThumbnails"
(contentChange)="contentChange.emit($event)"
(deselectObject)="deselectObject.emit($event)"
(selectObject)="selectObject.emit($event)">

View File

@@ -52,6 +52,11 @@ export class SearchResultsComponent {
*/
@Input() showCsvExport = false;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* The current sorting configuration of the search
*/

View File

@@ -21,7 +21,7 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m
templateUrl: '../../theme-support/themed.component.html',
})
export class ThemedSearchResultsComponent extends ThemedComponent<SearchResultsComponent> {
protected inAndOutputNames: (keyof SearchResultsComponent & keyof this)[] = ['linkType', 'searchResults', 'searchConfig', 'showCsvExport', 'sortConfig', 'viewMode', 'configuration', 'disableHeader', 'selectable', 'context', 'hidePaginationDetail', 'selectionConfig', 'contentChange', 'deselectObject', 'selectObject'];
protected inAndOutputNames: (keyof SearchResultsComponent & keyof this)[] = ['linkType', 'searchResults', 'searchConfig', 'showCsvExport', 'showThumbnails', 'sortConfig', 'viewMode', 'configuration', 'disableHeader', 'selectable', 'context', 'hidePaginationDetail', 'selectionConfig', 'contentChange', 'deselectObject', 'selectObject'];
@Input() linkType: CollectionElementLinkType;
@@ -31,6 +31,8 @@ export class ThemedSearchResultsComponent extends ThemedComponent<SearchResultsC
@Input() showCsvExport: boolean;
@Input() showThumbnails: boolean;
@Input() sortConfig: SortOptions;
@Input() viewMode: ViewMode;

View File

@@ -38,6 +38,7 @@
[selectable]="selectable"
[selectionConfig]="selectionConfig"
[showCsvExport]="showCsvExport"
[showThumbnails]="showThumbnails"
(contentChange)="onContentChange($event)"
(deselectObject)="deselectObject.emit($event)"
(selectObject)="selectObject.emit($event)"></ds-themed-search-results>

View File

@@ -131,6 +131,11 @@ export class SearchComponent implements OnInit {
*/
@Input() showSidebar = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* Whether to show the view mode switch
*/

View File

@@ -19,7 +19,7 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode
templateUrl: '../theme-support/themed.component.html',
})
export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics', 'query'];
protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showThumbnails', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics', 'query'];
@Input() configurationList: SearchConfigurationOption[];
@@ -51,6 +51,8 @@ export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
@Input() showSidebar: boolean;
@Input() showThumbnails;
@Input() showViewModes: boolean;
@Input() useUniquePageId: boolean;

View File

@@ -282,6 +282,8 @@ import { GroupSearchBoxComponent } from './eperson-group-list/group-search-box/g
import {
ThemedItemPageTitleFieldComponent
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
import { NgxPaginationModule } from 'ngx-pagination';
const MODULES = [
CommonModule,
@@ -296,7 +298,8 @@ const MODULES = [
RouterModule,
DragDropModule,
GoogleRecaptchaModule,
MenuModule
MenuModule,
NgxPaginationModule
];
const ROOT_MODULES = [
@@ -416,6 +419,7 @@ const ENTRY_COMPONENTS = [
SearchResultGridElementComponent,
ItemListElementComponent,
ItemGridElementComponent,
BitstreamListItemComponent,
ItemSearchResultListElementComponent,
ItemSearchResultGridElementComponent,
BrowseEntryListElementComponent,

View File

@@ -220,6 +220,22 @@
"admin.registries.schema.title": "Metadata Schema Registry",
"admin.access-control.bulk-access.breadcrumbs": "Bulk Access Management",
"administrativeBulkAccess.search.results.head": "Search Results",
"admin.access-control.bulk-access": "Bulk Access Management",
"admin.access-control.bulk-access.title": "Bulk Access Management",
"admin.access-control.bulk-access-browse.header": "Step 1: Select Objects",
"admin.access-control.bulk-access-browse.search.header": "Search",
"admin.access-control.bulk-access-browse.selected.header": "Current selection({{number}})",
"admin.access-control.bulk-access-settings.header": "Step 2: Operation to Perform",
"admin.access-control.epeople.actions.delete": "Delete EPerson",
"admin.access-control.epeople.actions.impersonate": "Impersonate EPerson",
@@ -950,6 +966,10 @@
"collection.edit.return": "Back",
"collection.edit.tabs.access-control.head": "Access Control",
"collection.edit.tabs.access-control.title": "Collection Edit - Access Control",
"collection.edit.tabs.curate.head": "Curate",
"collection.edit.tabs.curate.title": "Collection Edit - Curate",
@@ -1186,6 +1206,10 @@
"community.edit.tabs.curate.title": "Community Edit - Curate",
"community.edit.tabs.access-control.head": "Access Control",
"community.edit.tabs.access-control.title": "Community Edit - Access Control",
"community.edit.tabs.metadata.head": "Edit Metadata",
"community.edit.tabs.metadata.title": "Community Edit - Metadata",
@@ -2226,6 +2250,10 @@
"item.edit.curate.title": "Curate Item: {{item}}",
"item.edit.tabs.access-control.head": "Access Control",
"item.edit.tabs.access-control.title": "Item Edit - Access Control",
"item.edit.tabs.metadata.head": "Metadata",
"item.edit.tabs.metadata.title": "Item Edit - Metadata",
@@ -2772,6 +2800,8 @@
"menu.section.access_control_authorizations": "Authorizations",
"menu.section.access_control_bulk": "Bulk Access Management",
"menu.section.access_control_groups": "Groups",
"menu.section.access_control_people": "People",
@@ -5135,4 +5165,55 @@
"admin.system-wide-alert.breadcrumbs": "System-wide Alerts",
"admin.system-wide-alert.title": "System-wide Alerts",
}
"item-access-control-title": "This form allows you to perform changes to the access conditions of the item's metadata or its bitstreams.",
"collection-access-control-title": "This form allows you to perform changes to the access conditions of all the items owned by this collection. Changes may be performed to either all Item metadata or all content (bitstreams).",
"community-access-control-title": "This form allows you to perform changes to the access conditions of all the items owned by any collection under this community. Changes may be performed to either all Item metadata or all content (bitstreams).",
"access-control-item-header-toggle": "Item's Metadata",
"access-control-bitstream-header-toggle": "Bitstreams",
"access-control-mode": "Mode",
"access-control-access-conditions": "Access conditions",
"access-control-no-access-conditions-warning-message": "Currently, no access conditions are specified below. If executed, this will replace the current access conditions with the default access conditions inherited from the owning collection.",
"access-control-replace-all": "Replace access conditions",
"access-control-add-to-existing": "Add to existing ones",
"access-control-limit-to-specific": "Limit the changes to specific bitstreams",
"access-control-process-all-bitstreams": "Update all the bitstreams in the item",
"access-control-bitstreams-selected": "bitstreams selected",
"access-control-cancel": "Cancel",
"access-control-execute": "Execute",
"access-control-add-more": "Add more",
"access-control-select-bitstreams-modal.title": "Select bitstreams",
"access-control-select-bitstreams-modal.no-items": "No items to show.",
"access-control-select-bitstreams-modal.close": "Close",
"access-control-option-label": "Access condition type",
"access-control-option-note": "Choose an access condition to apply to selected objects.",
"access-control-option-start-date": "Grant access from",
"access-control-option-start-date-note": "Select the date from which the related access condition is applied",
"access-control-option-end-date": "Grant access until",
"access-control-option-end-date-note": "Select the date until which the related access condition is applied",
}