Fix: fix validation and translation

Show/hide the datepicker based on the value
This commit is contained in:
Enea Jahollari
2023-06-01 15:27:11 +02:00
parent 23eeee3d16
commit c1dcebbd04
10 changed files with 214 additions and 242 deletions

View File

@@ -1,65 +1,87 @@
<form [formGroup]="form">
<div *ngIf="mode === 'replace' && allControlsAreEmpty && form.status !== 'DISABLED'" class="alert alert-warning">
<form #ngForm="ngForm">
<div *ngIf="mode === 'replace' && allControlsAreEmpty && ngForm.enabled"
class="alert alert-warning">
{{'access-control-no-access-conditions-warning-message' | translate}}
</div>
<ng-container *ngFor="let control of accessControl.controls; let i = index">
<div [formGroup]="$any(control)"
class="mt-3" data-testId="access-control-item"
style="display: grid; grid-template-columns: 1fr 50px; grid-gap: 10px">
<ng-container *ngFor="let control of form.accessControls; let i = index">
<div ngModelGroup="access-control-item-{{i}}" class="access-control-item mt-3">
<div class="d-flex flex-column">
<div>
<label for="accesscontroloption">{{'access-control-option-label' | translate}}</label>
<select id="accesscontroloption" formControlName="itemName" class="form-control">
<label for="accesscontroloption-{{i}}">
{{'access-control-option-label' | translate}}
</label>
<select
id="accesscontroloption-{{i}}"
[(ngModel)]="control.itemName"
(ngModelChange)="accessControlChanged(control, $event)"
name="itemName-{{i}}"
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>
<small class="form-text text-muted">
{{'access-control-option-note' | translate}}
</small>
</div>
<div class="input-group" [class.d-none]="control.get('endDate')?.disabled">
<label for="accesscontrolstartdate">{{'access-control-option-start-date' | translate}}</label>
<input
id="accesscontrolstartdate"
class="form-control"
placeholder="yyyy-mm-dd"
name="dp"
formControlName="startDate"
[minDate]="control | maxStartDate: dropdownOptions"
ngbDatepicker
#d="ngbDatepicker"
/>
<div class="input-group-append">
<button class="btn btn-outline-secondary fas fa-calendar"
[disabled]="form.status === 'DISABLED'"
(click)="d.toggle()"
type="button"></button>
<div *ngIf="control.hasStartDate" class="mt-3">
<label for="accesscontrolstartdate-{{i}}">
{{'access-control-option-start-date' | translate}}
</label>
<div class="input-group">
<input
id="accesscontrolstartdate-{{i}}"
class="form-control"
placeholder="yyyy-mm-dd"
[(ngModel)]="control.startDate"
name="startDate-{{i}}"
[minDate]="control.maxStartDate | toDate"
ngbDatepicker
#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>
<small class="form-text text-muted">
{{'access-control-option-start-date-note' | translate}}
</small>
</div>
<div class="input-group" [class.d-none]="control.get('endDate')?.disabled">
<label for="accesscontrolenddate">{{'access-control-option-end-date' | translate}}</label>
<input
id="accesscontrolenddate"
class="form-control"
placeholder="yyyy-mm-dd"
name="dp"
formControlName="endDate"
[maxDate]="control | maxEndDate: dropdownOptions"
ngbDatepicker
#d1="ngbDatepicker"
/>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary fas fa-calendar"
[disabled]="form.status === 'DISABLED'"
(click)="d1.toggle()">
</button>
<div *ngIf="control.hasEndDate" class="mt-3">
<label for="accesscontrolenddate-{{i}}">
{{'access-control-option-end-date' | translate}}
</label>
<div class="input-group">
<input
id="accesscontrolenddate-{{i}}"
class="form-control"
placeholder="yyyy-mm-dd"
[(ngModel)]="control.endDate"
name="endDate-{{i}}"
[maxDate]="control.maxEndDate | toDate"
ngbDatepicker
#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>
<small class="form-text text-muted">
{{'access-control-option-end-date-note' | translate}}
</small>
</div>
</div>
@@ -68,18 +90,17 @@
<div class="input-group">
<button type="button" class="btn btn-outline-danger"
[disabled]="form.status === 'DISABLED'"
[disabled]="ngForm.disabled"
(click)="removeAccessControlItem(i)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</ng-container>
<button type="button" id="add-btn" class="btn btn-outline-primary mt-3"
[disabled]="form.status === 'DISABLED'"
<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}}

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

@@ -1,15 +1,14 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccessControlArrayFormComponent } from './access-control-array-form.component';
import { ReactiveFormsModule } from '@angular/forms';
import { SharedBrowseByModule } from '../../browse-by/shared-browse-by.module';
import { AccessControlArrayFormComponent, AccessControlItem } 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 { ControlMaxStartDatePipe } from './control-max-start-date.pipe';
import { ControlMaxEndDatePipe } from './control-max-end-date.pipe';
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;
@@ -17,8 +16,8 @@ describe('AccessControlArrayFormComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ CommonModule, ReactiveFormsModule, SharedBrowseByModule, TranslateModule, NgbDatepickerModule ],
declarations: [ AccessControlArrayFormComponent, ControlMaxStartDatePipe, ControlMaxEndDatePipe ]
imports: [ CommonModule, FormsModule, SharedBrowseByModule, TranslateModule, NgbDatepickerModule ],
declarations: [ AccessControlArrayFormComponent, ToDatePipe ]
})
.compileComponents();
});
@@ -27,6 +26,7 @@ describe('AccessControlArrayFormComponent', () => {
fixture = TestBed.createComponent(AccessControlArrayFormComponent);
component = fixture.componentInstance;
component.dropdownOptions = [{name: 'Option1'}, {name: 'Option2'}] as any;
component.type = 'item';
fixture.detectChanges();
});
@@ -35,37 +35,52 @@ describe('AccessControlArrayFormComponent', () => {
});
it('should have only one empty control access item in the form', () => {
const accessControlItems = fixture.debugElement.queryAll(By.css('[data-testId="access-control-item"]'));
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.accessControl.length).toEqual(2);
expect(component.form.accessControls.length).toEqual(2);
});
it('should remove access control item', () => {
component.removeAccessControlItem(0);
expect(component.accessControl.length).toEqual(0);
expect(component.form.accessControls.length).toEqual(0);
component.addAccessControlItem();
component.removeAccessControlItem(0);
expect(component.accessControl.length).toEqual(0);
expect(component.form.accessControls.length).toEqual(0);
});
it('should set access control item value', () => {
const item = { itemName: 'item1', startDate: '2022-01-01', endDate: '2022-02-01' };
const item: AccessControlItem = { itemName: 'item1', startDate: '2022-01-01', endDate: '2022-02-01' };
component.addAccessControlItem(item.itemName);
component.accessControl.controls[0].patchValue(item);
expect(component.form.value.accessControl[0]).toEqual(item);
// set value to item1
component.accessControlChanged(
component.form.accessControls[0],
'item1'
);
expect(component.form.accessControls[0].startDate).toEqual(item.startDate);
expect(component.form.accessControls[0].endDate).toEqual(item.endDate);
expect(component.form.accessControls[0].itemName).toEqual(item.itemName);
});
it('should reset form value', () => {
const item = { itemName: 'item1', startDate: '2022-01-01', endDate: '2022-02-01' };
component.addAccessControlItem(item.itemName);
component.accessControl.controls[1].patchValue(item);
// set value to item1
component.accessControlChanged(
component.form.accessControls[1],
'item1'
);
component.reset();
expect(component.form.value.accessControl[1].value).toEqual(undefined);
expect(component.form.accessControls[1].itemName).toEqual(undefined);
});
@@ -82,11 +97,11 @@ describe('AccessControlArrayFormComponent', () => {
});
it('should add new access control items when clicking "Add more" button', () => {
const addButton: DebugElement = fixture.debugElement.query(By.css('button#add-btn'));
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('[data-testId="access-control-item"]'));
const accessControlItems = fixture.debugElement.queryAll(By.css('.access-control-item'));
expect(accessControlItems.length).toEqual(2);
});
@@ -95,7 +110,7 @@ describe('AccessControlArrayFormComponent', () => {
removeButton.nativeElement.click();
fixture.detectChanges();
const accessControlItems = fixture.debugElement.queryAll(By.css('[data-testId="access-control-item"]'));
const accessControlItems = fixture.debugElement.queryAll(By.css('.access-control-item'));
expect(accessControlItems.length).toEqual(0);
});
});

View File

@@ -1,46 +1,35 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormControl } from '@angular/forms';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model';
import { dateToISOFormat } from '../../date.util';
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' ],
styleUrls: ['./access-control-array-form.component.scss'],
exportAs: 'accessControlArrayForm'
})
export class AccessControlArrayFormComponent implements OnInit, OnDestroy {
export class AccessControlArrayFormComponent implements OnInit {
@Input() dropdownOptions: AccessesConditionOption[] = [];
@Input() mode!: 'add' | 'replace';
@Input() type!: 'item' | 'bitstream';
private destroy$ = new Subject<void>();
@ViewChild('ngForm', {static: true}) ngForm!: NgForm;
form = this.fb.group({
accessControl: this.fb.array([])
});
constructor(private fb: FormBuilder) {}
form: { accessControls: AccessControlItem[] } = {
accessControls: []
};
ngOnInit(): void {
this.addAccessControlItem();
this.handleValidationOnFormArrayChanges();
}
/**
* Get the access control form array.
*/
get accessControl() {
return this.form.get('accessControl') as FormArray;
// Disable the form by default
setTimeout(() => this.disable(), 0);
}
get allControlsAreEmpty() {
if (this.accessControl.length === 0) {
return true;
}
return this.accessControl.value.every(x => x.itemName === null || x.itemName === '');
return this.form.accessControls
.every(x => x.itemName === null || x.itemName === '');
}
/**
@@ -49,11 +38,15 @@ export class AccessControlArrayFormComponent implements OnInit, OnDestroy {
* @param itemName The name of the item to add
*/
addAccessControlItem(itemName: string = null) {
this.accessControl.push(this.fb.group({
this.form.accessControls.push({
itemName,
startDate: new FormControl({ value: null, disabled: true }),
endDate: new FormControl({ value: null, disabled: true })
}));
startDate: null,
hasStartDate: false,
maxStartDate: null,
endDate: null,
hasEndDate: false,
maxEndDate: null,
});
}
/**
@@ -61,7 +54,7 @@ export class AccessControlArrayFormComponent implements OnInit, OnDestroy {
* @param index
*/
removeAccessControlItem(index: number) {
this.accessControl.removeAt(index);
this.form.accessControls.splice(index, 1);
}
/**
@@ -70,7 +63,7 @@ export class AccessControlArrayFormComponent implements OnInit, OnDestroy {
* @return The form value
*/
getValue() {
return (this.form.value.accessControl as any[])
return this.form.accessControls
.filter(x => x.itemName !== null && x.itemName !== '')
.map(x => ({
name: x.itemName,
@@ -83,77 +76,45 @@ export class AccessControlArrayFormComponent implements OnInit, OnDestroy {
* Set the value of the form from the parent component.
*/
reset() {
this.accessControl.reset([]);
this.form.accessControls = [];
}
/**
* Disable the form.
* This will be used to disable the form from the parent component.
* This will also disable all date controls.
*/
disable() {
this.form.disable();
// disable all date controls
for (const control of this.accessControl.controls) {
control.get('startDate').disable();
control.get('endDate').disable();
}
}
disable = () => this.ngForm.control.disable();
/**
* Enable the form.
* This will be used to enable the form from the parent component.
* This will also enable all date controls.
*/
enable() {
this.form.enable();
enable = () => this.ngForm.control.enable();
// enable date controls
for (const control of this.accessControl.controls) {
control.get('startDate').enable();
control.get('endDate').enable();
}
}
accessControlChanged(control: AccessControlItem, selectedItem: string) {
const item = this.dropdownOptions
.find((x) => x.name === selectedItem);
/**
* Handle validation on form array changes.
* This will be used to enable/disable date controls based on the selected item.
* @private
*/
private handleValidationOnFormArrayChanges() {
this.accessControl.valueChanges
.pipe(
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
takeUntil(this.destroy$)
)
.subscribe((value) => {
for (const [ index, controlValue ] of value.entries()) {
if (controlValue.itemName) {
const item = this.dropdownOptions.find((x) => x.name === controlValue.itemName);
const startDateCtrl = this.accessControl.controls[index].get('startDate');
const endDateCtrl = this.accessControl.controls[index].get('endDate');
control.startDate = null;
control.endDate = null;
if (item?.hasStartDate) {
startDateCtrl.enable({ emitEvent: false });
} else {
startDateCtrl.patchValue(null);
startDateCtrl.disable({ emitEvent: false });
}
if (item?.hasEndDate) {
endDateCtrl.enable({ emitEvent: false });
} else {
endDateCtrl.patchValue(null);
endDateCtrl.disable({ emitEvent: false });
}
}
}
});
}
control.hasStartDate = item?.hasStartDate || false;
control.hasEndDate = item?.hasEndDate || false;
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
control.maxStartDate = item?.maxStartDate || null;
control.maxEndDate = item?.maxEndDate || null;
}
}
export interface AccessControlItem {
itemName: string | null;
hasStartDate?: boolean;
startDate: string | null;
maxStartDate?: string | null;
hasEndDate?: boolean;
endDate: string | null;
maxEndDate?: string | null;
}

View File

@@ -1,26 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap/datepicker/ngb-date-struct';
import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model';
@Pipe({
// eslint-disable-next-line @angular-eslint/pipe-prefix
name: 'maxEndDate',
pure: false
})
export class ControlMaxEndDatePipe implements PipeTransform {
transform(control: AbstractControl, dropdownOptions: AccessesConditionOption[]): NgbDateStruct | null {
const { itemName } = control.value;
const item = dropdownOptions.find((x) => x.name === itemName);
if (!item?.hasEndDate) {
return null;
}
const date = new Date(item.maxEndDate);
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate()
} as NgbDateStruct;
}
}

View File

@@ -1,27 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap/datepicker/ngb-date-struct';
import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model';
@Pipe({
// eslint-disable-next-line @angular-eslint/pipe-prefix
name: 'maxStartDate',
pure: false
})
export class ControlMaxStartDatePipe implements PipeTransform {
transform(control: AbstractControl, dropdownOptions: AccessesConditionOption[]): NgbDateStruct | null {
const { itemName } = control.value;
const item = dropdownOptions.find((x) => x.name === itemName);
if (!item?.hasStartDate) {
return null;
}
const date = new Date(item.maxStartDate);
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate()
} as NgbDateStruct;
}
}

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

@@ -53,6 +53,7 @@
<ds-access-control-array-form
#itemAccessCmp
[type]="'item'"
[mode]="state.item.accessMode"
[dropdownOptions]="(dropdownData$ | async)?.itemAccessConditionOptions || []">
</ds-access-control-array-form>
@@ -141,6 +142,7 @@
<ds-access-control-array-form
#bitstreamAccessCmp
[type]="'bitstream'"
[mode]="state.bitstream.accessMode"
[dropdownOptions]="(dropdownData$ | async)?.bitstreamAccessConditionOptions || []">
</ds-access-control-array-form>
@@ -152,7 +154,7 @@
<div *ngIf="showSubmit" class="d-flex justify-content-end">
<button class="btn btn-outline-primary mr-3" (click)="reset()">
{{ 'access-control-reset' | translate }}
{{ 'access-control-cancel' | translate }}
</button>
<button class="btn btn-primary"
[disabled]="!state.item.toggleStatus && !state.bitstream.toggleStatus"

View File

@@ -1,35 +1,31 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { UiSwitchModule } from 'ngx-ui-switch';
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 {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 { ControlMaxStartDatePipe } from './access-control-array-form/control-max-start-date.pipe';
import { ControlMaxEndDatePipe } from './access-control-array-form/control-max-end-date.pipe';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
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
],
imports: [
CommonModule,
SharedModule,
TranslateModule,
UiSwitchModule,
NgbDatepickerModule
],
declarations: [
AccessControlFormContainerComponent,
AccessControlArrayFormComponent,
ItemAccessControlSelectBitstreamsModalComponent,
ControlMaxStartDatePipe,
ControlMaxEndDatePipe
ToDatePipe
],
exports: [ AccessControlFormContainerComponent, AccessControlArrayFormComponent ],
})

View File

@@ -227,17 +227,17 @@
"admin.access-control.bulk-access": "Bulk access",
"admin.access-control.bulk-access": "Bulk Access Management",
"admin.access-control.bulk-access.title": "Bulk Access",
"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.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.bulk-access-settings.header": "Step 2: Operation to Perform",
"admin.access-control.epeople.actions.delete": "Delete EPerson",
@@ -2903,7 +2903,7 @@
"menu.section.access_control_authorizations": "Authorizations",
"menu.section.access_control_bulk": "Bulk access",
"menu.section.access_control_bulk": "Bulk Access Management",
"menu.section.access_control_groups": "Groups",
@@ -5357,11 +5357,11 @@
"admin.system-wide-alert.title": "System-wide Alerts",
"item-access-control-title": "This form allows you to perform changes to the access condition of all the item's metadata and all its bitstreams.",
"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 condition of all the items owned by collection under this community. Changes can be performed on the access condition for the metadata (item) or for the content (bitstream).",
"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 condition of all the items owned by collection under this community. Changes can be performed on the access condition for the metadata (item) or for the content (bitstream).",
"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",
@@ -5371,7 +5371,7 @@
"access-control-access-conditions": "Access conditions",
"access-control-no-access-conditions-warning-message": "You have not specified any access conditions, the new items access conditions will be inherited from the owning collection.",
"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",
@@ -5379,11 +5379,11 @@
"access-control-limit-to-specific": "Limit the changes to specific bitstreams",
"access-control-process-all-bitstreams": "process all the bitstreams in the item",
"access-control-process-all-bitstreams": "Update all the bitstreams in the item",
"access-control-bitstreams-selected": "bitstreams selected",
"access-control-reset": "Reset",
"access-control-cancel": "Cancel",
"access-control-execute": "Execute",