Merge branch 'main' into DURACOM-195

This commit is contained in:
Davide Negretti
2024-02-02 10:41:47 +01:00
81 changed files with 513 additions and 316 deletions

View File

@@ -1,4 +1,5 @@
<div class="container">
<h1>{{ 'admin.access-control.bulk-access.title' | translate }}</h1>
<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>

View File

@@ -201,7 +201,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
deleteEPerson(ePerson: EPerson) {
if (hasValue(ePerson.id)) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = ePerson;
modalRef.componentInstance.name = this.dsoNameService.getName(ePerson);
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';

View File

@@ -478,7 +478,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
take(1),
switchMap((eperson: EPerson) => {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = eperson;
modalRef.componentInstance.name = this.dsoNameService.getName(eperson);
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';

View File

@@ -420,7 +420,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
delete() {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = group;
modalRef.componentInstance.name = this.dsoNameService.getName(group);
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-group.modal.info';
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-group.modal.cancel';

View File

@@ -1,5 +1,5 @@
<div class="container">
<h2 id="header">{{'admin.batch-import.page.header' | translate}}</h2>
<h1 id="header">{{'admin.batch-import.page.header' | translate}}</h1>
<p>{{'admin.batch-import.page.help' | translate}}</p>
<p *ngIf="dso">
selected collection: <b>{{getDspaceObjectName()}}</b>&nbsp;

View File

@@ -19,7 +19,7 @@
<table id="formats" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col" [attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"></th>
<th scope="col"><span class="sr-only">{{'admin.registries.bitstream-formats.table.selected' | translate}}</span></th>
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
@@ -35,6 +35,7 @@
[checked]="isSelected(bitstreamFormat) | async"
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
>
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
</label>
</td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>

View File

@@ -2,7 +2,7 @@
<div class="metadata-registry row">
<div class="col-12">
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h2>
<h1 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h1>
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
@@ -19,7 +19,7 @@
<table id="metadata-schemas" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"><span class="sr-only">{{'admin.registries.metadata.schemas.table.selected' | translate}}</span></th>
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
@@ -34,6 +34,7 @@
[checked]="isSelected(schema) | async"
(change)="selectMetadataSchema(schema, $event)"
>
<span class="sr-only">{{((isSelected(schema) | async) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
</label>
</td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>

View File

@@ -1,11 +1,11 @@
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div>
<ng-template #createHeader>
<h4>{{messagePrefix + '.create' | translate}}</h4>
<h2>{{messagePrefix + '.create' | translate}}</h2>
</ng-template>
<ng-template #editheader>
<h4>{{messagePrefix + '.edit' | translate}}</h4>
<h2>{{messagePrefix + '.edit' | translate}}</h2>
</ng-template>
<ds-form [formId]="formId"

View File

@@ -1,11 +1,11 @@
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div>
<ng-template #createHeader>
<h4>{{messagePrefix + '.create' | translate}}</h4>
<h2>{{messagePrefix + '.create' | translate}}</h2>
</ng-template>
<ng-template #editheader>
<h4>{{messagePrefix + '.edit' | translate}}</h4>
<h2>{{messagePrefix + '.edit' | translate}}</h2>
</ng-template>
<ds-form [formId]="formId"

View File

@@ -2,7 +2,7 @@
<div class="metadata-schema row">
<div class="col-12" *ngVar="(metadataSchema$ | async) as schema">
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{schema?.prefix}}"</h2>
<h1 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{schema?.prefix}}"</h1>
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:{ namespace: schema?.namespace } }}</p>
@@ -10,7 +10,7 @@
[metadataSchema]="schema"
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
<h2>{{'admin.registries.schema.fields.head' | translate}}</h2>
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
<ds-pagination
@@ -24,7 +24,7 @@
<table id="metadata-fields" class="table table-striped table-hover">
<thead>
<tr>
<th></th>
<th><span class="sr-only">{{'admin.registries.schema.fields.table.selected' | translate}}</span></th>
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
@@ -33,12 +33,11 @@
<tbody>
<tr *ngFor="let field of fields?.page"
[ngClass]="{'table-primary' : isActive(field) | async}">
<td>
<label class="mb-0">
<td *ngVar="(isSelected(field) | async) as selected">
<input type="checkbox"
[checked]="isSelected(field) | async"
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
[checked]="selected"
(change)="selectMetadataField(field, $event)">
</label>
</td>
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>

View File

@@ -124,7 +124,7 @@ export class WorkspaceItemAdminWorkflowActionsComponent implements OnInit {
*/
deleteSupervisionOrder(supervisionOrderEntry: SupervisionOrderListEntry) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = supervisionOrderEntry.group;
modalRef.componentInstance.name = this.dsoNameService.getName(supervisionOrderEntry.group);
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-supervision.modal.header';
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-supervision.modal.info';
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-supervision.modal.cancel';

View File

@@ -13,7 +13,7 @@
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload"
[alternateText]="'Collection Logo'">
[alternateText]="'collection.logo' | translate">
</ds-comcol-page-logo>
<!-- Handle -->

View File

@@ -1,17 +1,17 @@
<div class="container-fluid mb-2" *ngVar="(itemTemplateRD$ | async) as itemTemplateRD">
<label>{{ 'collection.edit.template.label' | translate}}</label>
<span class="d-inline-block mb-2">{{ 'collection.edit.template.label' | translate}}</span>
<div class="button-row space-children-mr">
<button *ngIf="!itemTemplateRD?.payload" class="btn btn-success" (click)="addItemTemplate()">
<i class="fas fa-plus"></i>
<i class="fas fa-plus" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{"collection.edit.template.add-button" | translate}}</span>
</button>
<button *ngIf="itemTemplateRD?.payload" class="btn btn-danger" (click)="deleteItemTemplate()">
<i class="fas fa-trash-alt"></i>
<i class="fas fa-trash-alt" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{"collection.edit.template.delete-button" | translate}}</span>
</button>
<button *ngIf="itemTemplateRD?.payload" class="btn btn-primary"
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/itemtemplate'">
<i class="fas fa-edit"></i>
<i class="fas fa-edit" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{"collection.edit.template.edit-button" | translate}}</span>
</button>
</div>

View File

@@ -4,9 +4,9 @@
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
class="example-tree-node show-more-node">
<div class="btn-group">
<button type="button" class="btn btn-default" cdkTreeNodeToggle>
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
</button>
<span aria-hidden="true" class="btn btn-default invisible" cdkTreeNodeToggle>
<span class="fa fa-chevron-right"></span>
</span>
<div class="align-middle pt-2">
<button *ngIf="!(dataSource.loading$ | async)" (click)="getNextPage(node)"
class="btn btn-outline-primary btn-sm" role="button">
@@ -24,15 +24,18 @@
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
class="example-tree-node expandable-node">
<div class="btn-group">
<button type="button" class="btn btn-default" cdkTreeNodeToggle
[title]="'toggle ' + dsoNameService.getName(node.payload)"
[attr.aria-label]="'toggle ' + dsoNameService.getName(node.payload)"
<button *ngIf="hasChild(null, node) | async" type="button" class="btn btn-default" cdkTreeNodeToggle
[attr.aria-label]="(node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) }"
(click)="toggleExpanded(node)"
[ngClass]="(hasChild(null, node)| async) ? 'visible' : 'invisible'"
[attr.data-test]="(hasChild(null, node)| async) ? 'expand-button' : ''">
data-test="expand-button">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
<span class="sr-only">{{ (node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) } }}</span>
</button>
<!--Don't render the button when non-expandable otherwise it's still accessible, instead render this placeholder-->
<span *ngIf="!(hasChild(null, node) | async)" aria-hidden="true" class="btn btn-default invisible">
<span class="fa fa-chevron-right"></span>
</span>
<div class="d-flex flex-row">
<span class="align-middle pt-2 lead">
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a>
@@ -44,10 +47,9 @@
<ds-truncatable [id]="node.id">
<div class="text-muted" cdkTreeNodePadding>
<div class="d-flex" *ngIf="node.payload.shortDescription">
<button type="button" class="btn btn-default invisible">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>
<span aria-hidden="true" class="btn btn-default invisible">
<span class="fa fa-chevron-right"></span>
</span>
<ds-truncatable-part [id]="node.id" [minLines]="3">
<span>{{node.payload.shortDescription}}</span>
</ds-truncatable-part>
@@ -56,10 +58,9 @@
</ds-truncatable>
<div class="d-flex" *ngIf="node===loadingNode && dataSource.loading$ | async"
cdkTreeNodePadding>
<button type="button" class="btn btn-default invisible">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>
<span aria-hidden="true" class="btn btn-default invisible">
<span class="fa fa-chevron-right"></span>
</span>
<ds-themed-loading class="ds-themed-loading"></ds-themed-loading>
</div>
</cdk-tree-node>
@@ -67,9 +68,9 @@
<cdk-tree-node *cdkTreeNodeDef="let node; when: !(hasChild && isShowMore)" cdkTreeNodePadding
class="example-tree-node childless-node">
<div class="btn-group">
<button type="button" class="btn btn-default" cdkTreeNodeToggle>
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
</button>
<span aria-hidden="true" class="btn btn-default invisible" cdkTreeNodeToggle>
<span class="fa fa-chevron-right"></span>
</span>
<h6 class="align-middle pt-2">
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a>
</h6>
@@ -77,10 +78,9 @@
<ds-truncatable [id]="node.id">
<div class="text-muted" cdkTreeNodePadding>
<div class="d-flex" *ngIf="node.payload.shortDescription">
<button type="button" class="btn btn-default invisible">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>
<span aria-hidden="true" class="btn btn-default invisible">
<span class="fa fa-chevron-right"></span>
</span>
<ds-truncatable-part [id]="node.id" [minLines]="3">
<span>{{node.payload.shortDescription}}</span>
</ds-truncatable-part>

View File

@@ -0,0 +1,4 @@
::ng-deep .fa-chevron-right::before {
display: block;
width: 16px;
}

View File

@@ -5,7 +5,7 @@ import { CommunityListService, showMoreFlatNode, toFlatNode } from '../community
import { CdkTreeModule } from '@angular/cdk/tree';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { Community } from '../../core/shared/community.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
@@ -300,12 +300,14 @@ describe('CommunityListComponent', () => {
describe('second top community node is expanded and has more children (collections) than page size of collection', () => {
describe('children of second top com are added (page-limited pageSize 2)', () => {
let allNodes;
let allNodes: DebugElement[];
beforeEach(fakeAsync(() => {
const chevronExpand = fixture.debugElement.queryAll(By.css('.expandable-node button'));
const chevronExpandSpan = fixture.debugElement.queryAll(By.css('.expandable-node button span'));
if (chevronExpandSpan[1].nativeElement.classList.contains('fa-chevron-right')) {
chevronExpand[1].nativeElement.click();
const toggleButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.expandable-node button'));
const toggleButtonText: DebugElement = toggleButtons[1].query(By.css('span'));
expect(toggleButtonText).not.toBeNull();
if (toggleButtonText.nativeElement.classList.contains('fa-chevron-right')) {
toggleButtons[1].nativeElement.click();
tick();
fixture.detectChanges();
}
@@ -315,17 +317,18 @@ describe('CommunityListComponent', () => {
allNodes = [...expandableNodesFound, ...childlessNodesFound];
}));
it('tree contains 2 (page-limited) top com, 2 (page-limited) coll of 2nd top com, a show more for those page-limited coll and show more for page-limited top com', () => {
mockTopFlatnodesUnexpanded.slice(0, 2).map((topFlatnode: FlatNode) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === topFlatnode.name);
})).toBeTruthy();
});
mockCollectionsPage1.map((coll) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === coll.name);
})).toBeTruthy();
});
const allNodeNames: string[] = allNodes.map((node: DebugElement) => node.nativeElement.innerText.trim());
expect(allNodes.length).toEqual(4);
const flatNodes: string[] = mockTopFlatnodesUnexpanded.slice(0, 2).map((flatNode: FlatNode) => flatNode.name);
for (const flatNode of flatNodes) {
expect(allNodeNames).toContain(flatNode);
}
expect(flatNodes.length).toBe(2);
const page1CollectionNames: string[] = mockCollectionsPage1.map((collection: Collection) => collection.name);
for (const collectionName of page1CollectionNames) {
expect(allNodeNames).toContain(collectionName);
}
expect(page1CollectionNames.length).toBe(2);
const showMoreEl = fixture.debugElement.queryAll(By.css('.show-more-node'));
expect(showMoreEl.length).toEqual(2);
});

View File

@@ -19,6 +19,7 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-community-list',
templateUrl: './community-list.component.html',
styleUrls: ['./community-list.component.scss'],
})
export class CommunityListComponent implements OnInit, OnDestroy {

View File

@@ -7,7 +7,7 @@
<!-- Community name -->
<ds-comcol-page-header [name]="dsoNameService.getName(communityPayload)"></ds-comcol-page-header>
<!-- Community logo -->
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'community.logo' | translate">
</ds-comcol-page-logo>
<!-- Handle -->
<ds-themed-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">

View File

@@ -7,11 +7,11 @@
<div class="form-group row">
<div class="col text-right space-children-mr">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
<i class="fas fa-times" aria-hidden="true"></i> {{'community.delete.cancel' | translate}}
</button>
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'community.delete.processing' | translate}}</span>
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}</span>
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'community.delete.processing' | translate}}</span>
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span>
</button>
</div>
</div>

View File

@@ -351,7 +351,28 @@ function addOperationToList(body: JsonPatchOperationObject[], actionType, target
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.move, from: fromPath, path: targetPath }));
break;
}
return newBody;
return dedupeOperationEntries(newBody);
}
/**
* Dedupe operation entries by op and path. This prevents processing unnecessary patches in a single PATCH request.
*
* @param body JSON patch operation object entries
* @returns deduped JSON patch operation object entries
*/
function dedupeOperationEntries(body: JsonPatchOperationObject[]): JsonPatchOperationObject[] {
const ops = new Map<string, any>();
for (let i = body.length - 1; i >= 0; i--) {
const patch = body[i].operation;
const key = `${patch.op}-${patch.path}`;
if (!ops.has(key)) {
ops.set(key, patch);
} else {
body.splice(i, 1);
}
}
return body;
}
function makeOperationEntry(operation) {

View File

@@ -2,23 +2,23 @@
<div class="button-row top d-flex my-2 space-children-mr ml-gap">
<button class="mr-auto btn btn-success" id="dso-add-btn" [disabled]="form.newValue || (saving$ | async)"
[title]="dsoType + '.edit.metadata.add-button' | translate"
(click)="add()"><i class="fas fa-plus"></i>
(click)="add()"><i class="fas fa-plus" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.add-button' | translate }}</span>
</button>
<button class="btn btn-warning ml-1" id="dso-reinstate-btn" *ngIf="isReinstatable" [disabled]="(saving$ | async)"
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
(click)="reinstate()"><i class="fas fa-undo-alt"></i>
(click)="reinstate()"><i class="fas fa-undo-alt" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.reinstate-button' | translate }}</span>
</button>
<button class="btn btn-primary ml-1" id="dso-save-btn" [disabled]="!hasChanges || (saving$ | async)"
[title]="dsoType + '.edit.metadata.save-button' | translate"
(click)="submit()"><i class="fas fa-save"></i>
(click)="submit()"><i class="fas fa-save" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.save-button' | translate }}</span>
</button>
<button class="btn btn-danger ml-1" id="dso-discard-btn" *ngIf="!isReinstatable"
[title]="dsoType + '.edit.metadata.discard-button' | translate"
[disabled]="!hasChanges || (saving$ | async)"
(click)="discard()"><i class="fas fa-times"></i>
(click)="discard()"><i class="fas fa-times" aria-hidden="true"></i>
<span class="d-none d-sm-inline">&nbsp;{{ dsoType + '.edit.metadata.discard-button' | translate }}</span>
</button>
</div>
@@ -74,16 +74,19 @@
<div class="mt-2 float-right space-children-mr ml-gap">
<button class="btn btn-warning" *ngIf="isReinstatable" [disabled]="(saving$ | async)"
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
(click)="reinstate()"><i class="fas fa-undo-alt"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
(click)="reinstate()">
<i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
</button>
<button class="btn btn-primary" [disabled]="!hasChanges || (saving$ | async)"
[title]="dsoType + '.edit.metadata.save-button' | translate"
(click)="submit()"><i class="fas fa-save"></i> {{ dsoType + '.edit.metadata.save-button' | translate }}
(click)="submit()">
<i class="fas fa-save" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.save-button' | translate }}
</button>
<button class="btn btn-danger" *ngIf="!isReinstatable"
[title]="dsoType + '.edit.metadata.discard-button' | translate"
[disabled]="!hasChanges || (saving$ | async)"
(click)="discard()"><i class="fas fa-times"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }}
(click)="discard()">
<i class="fas fa-times" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }}
</button>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<div class="w-100 position-relative">
<input type="text" #mdFieldInput
class="form-control" [ngClass]="{ 'is-invalid': showInvalid }"
[attr.aria-label]="(dsoType + '.edit.metadata.metadatafield') | translate"
[value]="mdField"
[formControl]="input"
(focusin)="query$.next(mdField)"
@@ -10,9 +11,9 @@
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (mdFieldOptions$ | async)?.length > 0}">
<div class="dropdown-list">
<div *ngFor="let mdFieldOption of (mdFieldOptions$ | async)">
<a href="javascript:void(0);" class="d-block dropdown-item" (click)="select(mdFieldOption)">
<button class="d-block dropdown-item" (click)="select(mdFieldOption)">
<span [innerHTML]="mdFieldOption"></span>
</a>
</button>
</div>
</div>
</div>

View File

@@ -11,7 +11,9 @@
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
[placeholder]="placeholder"
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
<input type="submit" class="d-none"/>
<button class="sr-only" type="submit">
{{'search.filters.search.submit' | translate}}
</button>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
<div class="dropdown-list">
<div *ngFor="let suggestionOption of suggestions">

View File

@@ -2,6 +2,7 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { OrgUnitInputSuggestionsComponent } from './org-unit-input-suggestions.component';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
let component: OrgUnitInputSuggestionsComponent;
let fixture: ComponentFixture<OrgUnitInputSuggestionsComponent>;
@@ -21,6 +22,7 @@ describe('OrgUnitInputSuggestionsComponent', () => {
declarations: [OrgUnitInputSuggestionsComponent],
imports: [
FormsModule,
TranslateModule.forRoot(),
],
providers: [
],

View File

@@ -12,7 +12,9 @@
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
[placeholder]="placeholder"
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
<input type="submit" class="d-none"/>
<button class="sr-only" type="submit">
{{'search.filters.search.submit' | translate}}
</button>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
<div class="dropdown-list">
<div *ngFor="let suggestionOption of suggestions">

View File

@@ -1,13 +1,13 @@
<div class="container" *ngIf="(registration$ |async)">
<h3 class="mb-4">{{'forgot-password.form.head' | translate}}</h3>
<h1 class="mb-4">{{'forgot-password.form.head' | translate}}</h1>
<div class="card mb-4">
<div class="card-header">{{'forgot-password.form.identification.header' | translate}}</div>
<div class="card-body">
<div class="row">
<div class="col-12">
<label class="font-weight-bold"
for="email">{{'forgot-password.form.identification.email' | translate}}</label>
<span id="email">{{(registration$ |async).email}}</span></div>
<span class="font-weight-bold">{{'forgot-password.form.identification.email' | translate}} </span>
<span [attr.data-test]="'email' | dsBrowserOnly">{{(registration$ |async).email}}</span>
</div>
</div>
</div>
</div>

View File

@@ -21,6 +21,7 @@ import {
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import { CoreState } from '../../core/core-state.model';
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
describe('ForgotPasswordFormComponent', () => {
let comp: ForgotPasswordFormComponent;
@@ -54,7 +55,10 @@ describe('ForgotPasswordFormComponent', () => {
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule],
declarations: [ForgotPasswordFormComponent],
declarations: [
BrowserOnlyPipe,
ForgotPasswordFormComponent,
],
providers: [
{provide: Router, useValue: router},
{provide: ActivatedRoute, useValue: route},
@@ -75,7 +79,7 @@ describe('ForgotPasswordFormComponent', () => {
describe('init', () => {
it('should initialise mail address', () => {
const elem = fixture.debugElement.queryAll(By.css('span#email'))[0].nativeElement;
const elem = fixture.debugElement.queryAll(By.css('span[data-test="email"]'))[0].nativeElement;
expect(elem.innerHTML).toContain('test@email.org');
});
});

View File

@@ -1,5 +1,5 @@
<div class="container" *ngIf="(healthResponseInitialised | async) && (healthInfoResponseInitialised | async)">
<h2>{{'health-page.heading' | translate}}</h2>
<h1>{{'health-page.heading' | translate}}</h1>
<div *ngIf="(healthResponse | async) && (healthInfoResponse | async)">
<ul ngbNav #nav="ngbNav" [activeId]="'status'" class="nav-tabs">
<li [ngbNavItem]="'status'" role="presentation">

View File

@@ -1,9 +1,8 @@
<div class="row row-offcanvas row-offcanvas-right">
<div class="col-xs-12 col-sm-12 col-md-9">
<form class="primary" [formGroup]="feedbackForm" (ngSubmit)="createFeedback()">
<h1>{{ 'info.feedback.head' | translate }}</h1>
<p>{{ 'info.feedback.info' | translate }}</p>
<fieldset class="col p-0">
<form [formGroup]="feedbackForm" (ngSubmit)="createFeedback()" class="col p-0">
<div class="row">
<div class="control-group col-sm-12">
<label class="control-label" for="email">{{ 'info.feedback.email-label' | translate }}&nbsp;</label>
@@ -39,7 +38,6 @@
<button [disabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>

View File

@@ -15,10 +15,10 @@
[attr.data-test]="'import-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission-external' | translate}}">
<i class="fa fa-file-import" aria-hidden="true"></i>
<span class="caret"></span>
<span class="caret" aria-hidden="true"></span>
</button>
<div ngbDropdownMenu
class="dropdown-menu"
class="dropdown-menu p-0"
id="importControlsDropdownMenu"
aria-labelledby="dropdownImport">
<ds-entity-dropdown [isSubmission]="false" (selectionChange)="openPage($event)"></ds-entity-dropdown>

View File

@@ -13,10 +13,10 @@
[attr.data-test]="'submission-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission' | translate}}">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
<span class="caret"></span>
<span class="caret" aria-hidden="true"></span>
</button>
<div ngbDropdownMenu
class="dropdown-menu"
class="dropdown-menu p-0"
id="entityControlsDropdownMenu"
aria-labelledby="dropdownSubmission">
<ds-entity-dropdown [isSubmission]="true" (selectionChange)="openDialog($event)"></ds-entity-dropdown>

View File

@@ -43,7 +43,8 @@ export class ProfilePageMetadataFormComponent implements OnInit {
new DynamicInputModel({
id: 'email',
name: 'email',
readOnly: true
readOnly: true,
disabled: true,
}),
new DynamicInputModel({
id: 'firstname',
@@ -55,6 +56,7 @@ export class ProfilePageMetadataFormComponent implements OnInit {
errorMessages: {
required: 'This field is required'
},
autoComplete: 'given-name',
}),
new DynamicInputModel({
id: 'lastname',
@@ -66,10 +68,12 @@ export class ProfilePageMetadataFormComponent implements OnInit {
errorMessages: {
required: 'This field is required'
},
autoComplete: 'family-name',
}),
new DynamicInputModel({
id: 'phone',
name: 'eperson.phone'
name: 'eperson.phone',
autoComplete: 'tel',
}),
new DynamicSelectModel<string>({
id: 'language',

View File

@@ -39,12 +39,14 @@ export class ProfilePageSecurityFormComponent implements OnInit {
new DynamicInputModel({
id: 'password',
name: 'password',
inputType: 'password'
inputType: 'password',
autoComplete: 'new-password',
}),
new DynamicInputModel({
id: 'passwordrepeat',
name: 'passwordrepeat',
inputType: 'password'
inputType: 'password',
autoComplete: 'new-password',
})
];
@@ -79,7 +81,8 @@ export class ProfilePageSecurityFormComponent implements OnInit {
id: 'current-password',
name: 'current-password',
inputType: 'password',
required: true
required: true,
autoComplete: 'current-password',
}));
}
if (this.passwordCanBeEmpty) {

View File

@@ -1,7 +1,7 @@
<ng-container *ngVar="(user$ | async) as user">
<div class="container" *ngIf="user">
<ng-container *ngIf="isResearcherProfileEnabled() | async">
<h2 class="mb-4">{{'profile.head' | translate}}</h2>
<h1>{{'profile.title' | translate}}</h1>
<ng-container>
<div class="card mb-4">
<div class="card-header">{{'profile.card.researcher' | translate}}</div>
<div class="card-body">

View File

@@ -88,7 +88,7 @@
</div>
<div>
<label>&nbsp;</label>
<span aria-hidden="true" class="mb-2 d-inline-block">&nbsp;</span>
<div class="input-group">
<button type="button" class="btn btn-outline-danger"

View File

@@ -23,10 +23,10 @@
</ui-switch>
</div>
<div class="row mt-3">
<div class="col-12 col-md-3">
<fieldset class="row mt-3">
<legend class="h4 col-12 col-md-3">
{{ 'access-control-mode' | translate }}
</div>
</legend>
<div class="col-12 col-md-8">
<div class="form-check">
<input class="form-check-input" type="radio"
@@ -47,7 +47,7 @@
</label>
</div>
</div>
</div>
</fieldset>
</div>
<div>
@@ -78,10 +78,10 @@
</div>
<div *ngIf="showLimitToSpecificBitstreams" class="row mt-3">
<div class="col-12">
<fieldset class="col-12">
<legend class="h4">
{{'access-control-limit-to-specific' | translate}}
</div>
<div class="col-12">
</legend>
<div class="form-check">
<input class="form-check-input" type="radio"
name="changesLimit" id="processAll" value="all"
@@ -111,14 +111,14 @@
</label>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-md-3">
<fieldset class="row mt-3">
<legend class="h4 col-12 col-md-3">
{{'access-control-mode' | translate}}
</div>
</legend>
<div class="col-12 col-md-8">
<div class="form-check">
<input class="form-check-input" type="radio"
@@ -139,7 +139,7 @@
</label>
</div>
</div>
</div>
</fieldset>
<div>
<h3 class="h4 mt-3">{{'access-control-access-conditions' | translate}}</h3>

View File

@@ -1,4 +1,4 @@
<div *ngIf="searchField" class="form-group w-100 pr-2 pl-2">
<div *ngIf="searchField" class="form-group w-100 pr-2 pl-2 my-2">
<input type="search"
class="form-control w-100"
(click)="$event.stopPropagation();"
@@ -6,9 +6,9 @@
[formControl]="searchField"
#searchFieldEl>
</div>
<div class="dropdown-divider"></div>
<ul class="scrollable-menu p-0"
aria-labelledby="dropdownMenuButton"
<div class="dropdown-divider m-0"></div>
<ul class="scrollable-menu p-0 m-0"
role="menu"
(scroll)="onScroll($event)"
infiniteScroll
[infiniteScrollDistance]="1.5"
@@ -18,12 +18,13 @@
[scrollWindow]="false"
(scrolled)="onScrollDown()">
<li class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoading | async)">
<li class="dropdown-item disabled" role="menuitem" *ngIf="searchListCollection?.length == 0 && !(isLoading | async)">
{{'submission.sections.general.no-collection' | translate}}
</li>
<ng-container *ngIf="searchListCollection?.length > 0">
<li *ngFor="let listItem of searchListCollection"
class="dropdown-item collection-item"
role="menuitem"
title="{{ listItem.collection.name }}"
(click)="onSelect(listItem)">
<div class="list-unstyled mb-0">
@@ -34,9 +35,10 @@
</div>
</li>
</ng-container>
<button class="dropdown-item disabled" *ngIf="(isLoading | async)">
<li *ngIf="(isLoading | async)">
<button class="dropdown-item disabled">
<ds-themed-loading message="{{'loading.default' | translate}}">
</ds-themed-loading>
</button>
</li>
</ul>

View File

@@ -4,7 +4,8 @@
overflow-x: hidden;
}
.collection-item {
li:not(:last-of-type), .dropdown-divider {
border-top: none;
border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
}

View File

@@ -1,13 +1,13 @@
<div class="container-fluid">
<div class="row">
<div class="col-12 d-inline-block">
<label>{{type.value + '.edit.logo.label' | translate}}</label>
<div class="col-12 d-inline-block mb-1">
<span>{{type.value + '.edit.logo.label' | translate}}</span>
</div>
<ng-container *ngVar="(dso?.logo | async)?.payload as logo">
<div class="col-12 d-inline-block alert" [ngClass]="{'alert-danger': markLogoForDeletion}" id="logo-section" *ngIf="logo">
<div class="row">
<div class="col-8 d-inline-block">
<ds-comcol-page-logo [logo]="logo"></ds-comcol-page-logo>
<ds-comcol-page-logo [alternateText]="type.value + '.logo.alt'" [logo]="logo"></ds-comcol-page-logo>
</div>
<div class="col-4 d-inline-block">
<div *ngIf="logo" class="btn-group btn-group-sm float-right" role="group">
@@ -27,6 +27,7 @@
</div>
<div *ngIf="!logo" class="col-12 d-inline-block">
<ds-uploader *ngIf="initializedUploaderOptions | async"
[ariaLabel]="type.value + '.browse.logo'"
[dropMsg]="type.value + '.edit.logo.upload'"
[dropOverDocumentMsg]="type.value + '.edit.logo.upload'"
[enableDragOverDocument]="true"
@@ -43,6 +44,6 @@
[displayCancel]="false"
(submitForm)="onSubmit()">
<button before (click)="back.emit()" class="btn btn-outline-secondary" type="button">
<i class="fas fa-arrow-left"></i> {{ type.value + '.edit.return' | translate }}
<i class="fas fa-arrow-left" aria-hidden="true"></i> {{ type.value + '.edit.return' | translate }}
</button>
</ds-form>

View File

@@ -7,7 +7,7 @@
<a class="btn btn-danger"
[routerLink]="((type === 'community') ? '/communities/' : '/collections/') + (dsoRD$ | async)?.payload.uuid + '/delete'"
data-test="delete-button">
<i class="fas fa-trash"></i> {{type + '.edit.delete' | translate}}</a>
<i class="fas fa-trash" aria-hidden="true"></i> {{type + '.edit.delete' | translate}}</a>
</div>
</div>
<div class="pt-2">
@@ -27,7 +27,7 @@
</div>
<div class="col-12 text-right">
<a *ngIf="!hideReturnButton" [routerLink]="getPageUrl((dsoRD$ | async)?.payload)" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{ type + '.edit.return' | translate }}
<i class="fas fa-arrow-left" aria-hidden="true"></i> {{ type + '.edit.return' | translate }}
</a>
</div>
</div>

View File

@@ -1,3 +1,3 @@
<div *ngIf="logo" class="dso-logo mb-3">
<img [src]="logo._links.content.href" class="img-fluid" [attr.alt]="alternateText ? alternateText : null" (error)="errorHandler($event)"/>
<img [src]="logo._links.content.href" class="w-100 img-fluid" [attr.alt]="alternateText ? alternateText : null" (error)="errorHandler($event)"/>
</div>

View File

@@ -0,0 +1,4 @@
img {
max-width: var(--ds-comcol-logo-max-width);
max-height: var(--ds-comcol-logo-max-height);
}

View File

@@ -1,18 +1,18 @@
<div>
<div class="modal-header">{{ headerLabel | translate:{ dsoName: dsoNameService.getName(dso) } }}
<div class="modal-header">{{ headerLabel | translate:{ dsoName: name } }}
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p>{{ infoLabel | translate:{ dsoName: dsoNameService.getName(dso) } }}</p>
<p>{{ infoLabel | translate:{ dsoName: name } }}</p>
</div>
<div class="modal-footer">
<button type="button" class="cancel btn btn-outline-secondary" (click)="cancelPressed()" aria-label="Cancel">
<i class="fas fa-times"></i> {{ cancelLabel | translate:{ dsoName: dsoNameService.getName(dso) } }}
<i class="fas fa-times"></i> {{ cancelLabel | translate:{ dsoName: name } }}
</button>
<button type="button" class="confirm btn btn-{{brandColor}}" (click)="confirmPressed()" aria-label="Confirm" ngbAutofocus>
<i *ngIf="confirmIcon" class="{{confirmIcon}}"></i> {{ confirmLabel | translate:{ dsoName: dsoNameService.getName(dso) } }}
<i *ngIf="confirmIcon" class="{{confirmIcon}}"></i> {{ confirmLabel | translate:{ dsoName: name } }}
</button>
</div>
</div>

View File

@@ -1,7 +1,5 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-confirmation-modal',
@@ -18,7 +16,7 @@ export class ConfirmationModalComponent {
*/
@Input() brandColor = 'primary';
@Input() dso: DSpaceObject;
@Input() name: string;
/**
* An event fired when the cancel or confirm button is clicked, with respectively false or true
@@ -28,7 +26,6 @@ export class ConfirmationModalComponent {
constructor(
protected activeModal: NgbActiveModal,
public dsoNameService: DSONameService,
) {
}

View File

@@ -22,7 +22,7 @@
class="dropdown-menu"
id="dsSelectDropdownMenu"
aria-labelledby="dsSelectMenuButton">
<div aria-labelledby="dropdownMenuButton">
<div>
<ng-content select=".menu"></ng-content>
</div>
</div>

View File

@@ -1,25 +1,21 @@
<div *ngIf="!canActivate" class="dso-button-menu mb-1"
[title]="itemModel.text | translate"
[ngbTooltip]="itemModel.text | translate">
<a *ngIf="!section.model.disabled" class="d-flex flex-row flex-nowrap"
[routerLink]="itemModel.link"
href="javascript:void(0);">
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" [disabled]="section.model.disabled">
<i class="fas fa-{{section.icon}} fa-fw"></i>
</button>
<a *ngIf="!section.model.disabled" class="btn btn-dark btn-sm"
[routerLink]="itemModel.link">
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
<span class="sr-only">{{itemModel.text | translate}}</span>
</a>
<div *ngIf="section.model.disabled" class="d-flex flex-row flex-nowrap">
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" [disabled]="section.model.disabled">
<i class="fas fa-{{section.icon}} fa-fw"></i>
<button *ngIf="section.model.disabled" class="btn btn-dark btn-sm" [disabled]="true">
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
<span class="sr-only">{{itemModel.text | translate}}</span>
</button>
</div>
</div>
<div *ngIf="canActivate" class="dso-button-menu mb-1"
[title]="itemModel.text | translate"
[ngbTooltip]="itemModel.text | translate">
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" [disabled]="section.model.disabled"
<button class="btn btn-dark btn-sm" [disabled]="section.model.disabled"
(click)="activate($event)">
<i class="fas fa-{{section.icon}} fa-fw"></i>
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
<span class="sr-only">{{itemModel.text | translate}}</span>
</button>
</div>

View File

@@ -96,7 +96,7 @@ describe('DsoEditMenuSectionComponent', () => {
});
describe('when the section model in a disabled link or text', () => {
it('should show just the button', () => {
const textButton = fixture.debugElement.query(By.css('div div button'));
const textButton = fixture.debugElement.query(By.css('div a'));
expect(textButton.nativeElement.innerHTML).toContain('fa-' + iconString);
});
});
@@ -144,7 +144,7 @@ describe('DsoEditMenuSectionComponent', () => {
});
describe('link model', () => {
describe('when the section model in a non disabled link', () => {
initAsync(dummySectionLink, menuService);
beforeEach(() => {
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
@@ -154,11 +154,8 @@ describe('DsoEditMenuSectionComponent', () => {
fixture.detectChanges();
});
describe('when the section model in a non disabled link', () => {
it('should show a link element with the button in it', () => {
const link = fixture.debugElement.query(By.css('a'));
expect(link.nativeElement.innerHTML).toContain('button');
});
it('should show the link element', () => {
expect(fixture.debugElement.query(By.css('a'))).not.toBeNull();
});
});

View File

@@ -2,6 +2,7 @@
<input type="search"
class="form-control"
(click)="$event.stopPropagation();"
[attr.aria-label]="'dso-selector.placeholder' | translate: { type: typesString }"
placeholder="{{'dso-selector.placeholder' | translate: { type: typesString } }}"
[formControl]="input" ngbAutofocus (keyup.enter)="selectSingleResult()">
</div>

View File

@@ -75,6 +75,11 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
*/
@Input() sort: SortOptions;
/**
* The id that should be given to the input box, this is required for accessibility reasons
*/
@Input() searchBoxId: string | null = null;
// list of allowed selectable dsoTypes
typesString: string;

View File

@@ -6,14 +6,14 @@
</div>
<div class="modal-body">
<button class="btn btn-outline-primary btn-lg btn-block" (click)="selectObject(undefined)">{{'dso-selector.create.community.top-level' | translate}}</button>
<h3 class="position-relative py-1 my-3 font-weight-normal">
<div class="h3 position-relative py-1 my-3 font-weight-normal">
<hr>
<div id="create-community-or-separator" class="text-center position-absolute w-100">
<span class="px-4 bg-white">{{'dso-selector.create.community.or-divider' | translate}}</span>
</div>
</h3>
</div>
<h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5>
<span class="h5 px-2">{{'dso-selector.create.community.sub-level' | translate}}</span>
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" [sort]="defaultSort" (onSelect)="selectObject($event)"></ds-dso-selector>
</div>
</div>

View File

@@ -5,7 +5,7 @@
</button>
</div>
<div class="modal-body">
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
<span *ngIf="header" class="h5 px-2">{{header | translate}}</span>
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid"
[entityType]="entityType"
[types]="selectorTypes"

View File

@@ -5,7 +5,7 @@
</button>
</div>
<div class="modal-body">
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
<span *ngIf="header" class="h5 px-2">{{header | translate}}</span>
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" [sort]="defaultSort" (onSelect)="selectObject($event)"></ds-dso-selector>
</div>
</div>

View File

@@ -5,7 +5,7 @@
</button>
</div>
<div class="modal-body">
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
<span *ngIf="header" class="h5 px-2">{{header | translate}}</span>
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
</div>
</div>

View File

@@ -20,6 +20,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
/**
* Component to wrap a list of existing dso's inside a modal
@@ -38,6 +39,7 @@ export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperCompone
protected notificationsService: NotificationsService, protected translationService: TranslateService,
protected scriptDataService: ScriptDataService,
protected authorizationDataService: AuthorizationDataService,
protected dsoNameService: DSONameService,
private modalService: NgbModal) {
super(activeModal, route);
}
@@ -49,7 +51,7 @@ export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperCompone
navigate(dso: DSpaceObject): Observable<boolean> {
if (dso instanceof Collection) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = dso;
modalRef.componentInstance.name = this.dsoNameService.getName(dso);
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-batch.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-batch.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-batch.cancel';

View File

@@ -21,6 +21,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
/**
* Component to wrap a list of existing dso's inside a modal
@@ -39,6 +40,7 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
protected notificationsService: NotificationsService, protected translationService: TranslateService,
protected scriptDataService: ScriptDataService,
protected authorizationDataService: AuthorizationDataService,
protected dsoNameService: DSONameService,
private modalService: NgbModal) {
super(activeModal, route);
}
@@ -50,7 +52,7 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
navigate(dso: DSpaceObject): Observable<boolean> {
if (dso instanceof Collection || dso instanceof Community) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = dso;
modalRef.componentInstance.name = this.dsoNameService.getName(dso);
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-metadata.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-metadata.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-metadata.cancel';

View File

@@ -1,6 +1,5 @@
<div
class="scrollable-menu"
aria-labelledby="dropdownMenuButton"
<ul class="scrollable-menu list-unstyled mb-0"
role="menu"
(scroll)="onScroll($event)"
infiniteScroll
[infiniteScrollDistance]="5"
@@ -9,20 +8,23 @@
[fromRoot]="true"
[scrollWindow]="false"
(scrolled)="onScrollDown()">
<button class="dropdown-item disabled" *ngIf="searchListEntity?.length == 0 && !(isLoadingList | async)">
<li *ngIf="searchListEntity?.length == 0 && !(isLoadingList | async)">
<button class="dropdown-item disabled" role="menuitem">
{{'submission.sections.general.no-entity' | translate}}
</button>
<button *ngFor="let listItem of searchListEntity"
class="dropdown-item entity-item"
</li>
<li *ngFor="let listItem of searchListEntity" class="entity-item text-primary">
<button class="dropdown-item"
role="menuitem"
title="{{ listItem.label }}"
(click)="onSelect(listItem)">
<ul class="list-unstyled mb-0">
<li class="list-item text-truncate text-primary font-weight-bold">{{ listItem.label.toLowerCase() + '.listelement.badge' | translate }}</li>
</ul>
<span class="text-truncate font-weight-bold">{{ listItem.label.toLowerCase() + '.listelement.badge' | translate }}</span>
</button>
<button class="dropdown-item disabled" *ngIf="(isLoadingList | async)" >
</li>
<li *ngIf="(isLoadingList | async)">
<button class="dropdown-item disabled" role="menuitem">
<ds-themed-loading message="{{'loading.default' | translate}}">
</ds-themed-loading>
</button>
</div>
</li>
</ul>

View File

@@ -1,6 +1,10 @@
.list-item:active {
.dropdown-item {
padding: 0.35rem 1rem;
&:active {
color: white !important;
}
}
.scrollable-menu {
height: auto;
@@ -8,7 +12,7 @@
overflow-x: hidden;
}
.entity-item {
li:not(:last-of-type) .dropdown-item {
border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
}

View File

@@ -123,7 +123,7 @@ describe('EntityDropdownComponent', () => {
scheduler.flush();
spyOn(component, 'onSelect');
const entityItem = fixture.debugElement.query(By.css('.entity-item:nth-child(2)'));
const entityItem = fixture.debugElement.query(By.css('.entity-item:nth-child(2) button'));
entityItem.triggerEventHandler('click', null);
scheduler.schedule(() => fixture.detectChanges());

View File

@@ -88,7 +88,6 @@
aria-expanded="false"
[attr.aria-labelledby]="'label_' + model.id">
<div class="scrollable-menu"
aria-labelledby="scrollableDropdownMenuButton"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"

View File

@@ -27,7 +27,7 @@
(keydown)="selectOnKeyDown($event, sdRef)">
</div>
<div ngbDropdownMenu
<div #dropdownMenu ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
[attr.aria-label]="model.placeholder">
<div class="scrollable-menu"
@@ -41,7 +41,8 @@
[scrollWindow]="false">
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList; let i = index"
[class.active]="i === selectedIndex"
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
title="{{ listEntry.display }}" role="option"
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">

View File

@@ -1,8 +1,17 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ElementRef
} from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { Observable, of as observableOf } from 'rxjs';
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { catchError, map, tap } from 'rxjs/operators';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
@@ -28,6 +37,8 @@ import { FormFieldMetadataValueObject } from '../../../models/form-field-metadat
templateUrl: './dynamic-scrollable-dropdown.component.html'
})
export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
@ViewChild('dropdownMenu', { read: ElementRef }) dropdownMenu: ElementRef;
@Input() bindId = true;
@Input() group: UntypedFormGroup;
@Input() model: DynamicScrollableDropdownModel;
@@ -40,6 +51,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
public loading = false;
public pageInfo: PageInfo;
public optionsList: any;
public inputText: string = null;
public selectedIndex = 0;
public acceptableKeys = ['Space', 'NumpadMultiply', 'NumpadAdd', 'NumpadSubtract', 'NumpadDecimal', 'Semicolon', 'Equal', 'Comma', 'Minus', 'Period', 'Quote', 'Backquote'];
constructor(protected vocabularyService: VocabularyService,
protected cdr: ChangeDetectorRef,
@@ -54,32 +68,26 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
*/
ngOnInit() {
this.updatePageInfo(this.model.maxOptions, 1);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(buildPaginatedList(
new PageInfo(),
[]
))
))
.subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = list.page;
if (this.model.value) {
this.setCurrentValue(this.model.value, true);
this.loadOptions();
}
loadOptions() {
this.loading = true;
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(buildPaginatedList(new PageInfo(), []))),
tap(() => this.loading = false)
).subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = list.page;
this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.selectedIndex = 0;
this.cdr.detectChanges();
});
this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged())
.subscribe((value) => {
this.setCurrentValue(value);
});
}
/**
@@ -94,10 +102,30 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
openDropdown(sdRef: NgbDropdown) {
if (!this.model.readOnly) {
this.group.markAsUntouched();
this.inputText = null;
this.updatePageInfo(this.model.maxOptions, 1);
this.loadOptions();
sdRef.open();
}
}
navigateDropdown(event: KeyboardEvent) {
if (event.key === 'ArrowDown') {
this.selectedIndex = Math.min(this.selectedIndex + 1, this.optionsList.length - 1);
} else if (event.key === 'ArrowUp') {
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
}
this.scrollToSelected();
}
scrollToSelected() {
const dropdownItems = this.dropdownMenu.nativeElement.querySelectorAll('.dropdown-item');
const selectedItem = dropdownItems[this.selectedIndex];
if (selectedItem) {
selectedItem.scrollIntoView({ block: 'nearest' });
}
}
/**
* KeyDown handler to allow toggling the dropdown via keyboard
* @param event KeyboardEvent
@@ -106,13 +134,54 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
selectOnKeyDown(event: KeyboardEvent, sdRef: NgbDropdown) {
const keyName = event.key;
if (keyName === ' ' || keyName === 'Enter') {
if (keyName === 'Enter') {
event.preventDefault();
event.stopPropagation();
sdRef.toggle();
} else if (keyName === 'ArrowDown' || keyName === 'ArrowUp') {
this.openDropdown(sdRef);
if (sdRef.isOpen()) {
this.onSelect(this.optionsList[this.selectedIndex]);
sdRef.close();
} else {
sdRef.open();
}
} else if (keyName === 'ArrowDown' || keyName === 'ArrowUp') {
event.preventDefault();
event.stopPropagation();
this.navigateDropdown(event);
} else if (keyName === 'Backspace') {
this.removeKeyFromInput();
} else if (this.isAcceptableKey(keyName)) {
this.addKeyToInput(keyName);
}
}
addKeyToInput(keyName: string) {
if (this.inputText === null) {
this.inputText = '';
}
this.inputText += keyName;
// When a new key is added, we need to reset the page info
this.updatePageInfo(this.model.maxOptions, 1);
this.loadOptions();
}
removeKeyFromInput() {
if (this.inputText !== null) {
this.inputText = this.inputText.slice(0, -1);
if (this.inputText === '') {
this.inputText = null;
}
this.loadOptions();
}
}
isAcceptableKey(keyPress: string): boolean {
// allow all letters and numbers
if (keyPress.length === 1 && keyPress.match(/^[a-zA-Z0-9]*$/)) {
return true;
}
// Some other characters like space, dash, etc should be allowed as well
return this.acceptableKeys.includes(keyPress);
}
/**
@@ -127,7 +196,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
this.pageInfo.totalElements,
this.pageInfo.totalPages
);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(buildPaginatedList(
new PageInfo(),

View File

@@ -58,11 +58,12 @@
<div class="col text-right space-children-mr">
<ng-content select="[before]"></ng-content>
<button *ngIf="displayCancel" type="reset" class="btn btn-outline-secondary" (click)="reset()">
<i class="fa fa-times"></i> {{cancelLabel | translate}}
<i class="fa fa-times" aria-hidden="true"></i> {{cancelLabel | translate}}
</button>
<ng-content select="[between]"></ng-content>
<button *ngIf="displaySubmit" type="submit" class="btn btn-primary" (click)="onSubmit()"
[disabled]="!(isValid() | async)"><i class="fas fa-save"></i> {{submitLabel | translate}}
[disabled]="!(isValid() | async)">
<i class="fas fa-save" aria-hidden="true"></i> {{submitLabel | translate}}
</button>
<ng-content select="[after]"></ng-content>
</div>

View File

@@ -9,7 +9,9 @@
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
[placeholder]="placeholder"
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
<input type="submit" class="d-none"/>
<button class="sr-only" type="submit">
{{'search.filters.search.submit' | translate}}
</button>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
<div class="dropdown-list">
<div *ngFor="let suggestionOption of suggestions">

View File

@@ -20,10 +20,9 @@
[ngModelOptions]="{standalone: true}" autocomplete="off"
/>
</ng-template>
<label class="d-none">
<input type="submit"/>
<span>{{'search.filters.search.submit' | translate}}</span>
</label>
<button class="sr-only" type="submit">
{{'search.filters.search.submit' | translate}}
</button>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
<div class="dropdown-list">
<div *ngFor="let suggestionOption of suggestions">

View File

@@ -9,7 +9,9 @@
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
[placeholder]="placeholder"
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
<input type="submit" class="d-none"/>
<button class="sr-only" type="submit">
{{'search.filters.search.submit' | translate}}
</button>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
<div class="dropdown-list">
<div *ngFor="let suggestionOption of suggestions">

View File

@@ -10,7 +10,9 @@
[placeholder]="placeholder"
ng-model-options="{standalone: true}"
autocomplete="off">
<input type="submit" class="d-none"/>
<button class="sr-only" type="submit">
{{'search.filters.search.submit' | translate}}
</button>
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
<div class="dropdown-list">
<div *ngFor="let suggestionOption of suggestions">

View File

@@ -1,9 +1,8 @@
<form class="form-login"
(ngSubmit)="submit()"
[formGroup]="form" novalidate>
<label class="sr-only">{{"login.form.email" | translate}}</label>
<input [attr.aria-label]="'login.form.email' |translate"
autocomplete="off"
autocomplete="username"
autofocus
class="form-control form-control-lg position-relative"
formControlName="email"
@@ -11,9 +10,8 @@
required
type="email"
[attr.data-test]="'email' | dsBrowserOnly">
<label class="sr-only">{{"login.form.password" | translate}}</label>
<input [attr.aria-label]="'login.form.password' |translate"
autocomplete="off"
autocomplete="current-password"
class="form-control form-control-lg position-relative mb-3"
placeholder="{{'login.form.password' | translate}}"
formControlName="password"

View File

@@ -11,7 +11,7 @@
<table id="collection-select" class="table table-striped table-hover">
<thead>
<tr>
<th aria-hidden="true"></th>
<th><span class="sr-only">{{'collection.select.table.selected' | translate}}</span></th>
<th scope="col">{{'collection.select.table.title' | translate}}</th>
</tr>
</thead>

View File

@@ -11,7 +11,7 @@
<table id="item-select" class="table table-striped table-hover">
<thead>
<tr>
<th aria-hidden="true"></th>
<th><span class="sr-only">{{'item.select.table.selected' | translate}}</span></th>
<th *ngIf="!hideCollection" scope="col">{{'item.select.table.collection' | translate}}</th>
<th scope="col">{{'item.select.table.author' | translate}}</th>
<th scope="col">{{'item.select.table.title' | translate}}</th>

View File

@@ -7,15 +7,29 @@
</div>
<div class="col">
<div *ngIf="!hideGear" ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right">
<button class="btn btn-secondary" id="paginationControls" ngbDropdownToggle [title]="'pagination.options.description' | translate" [attr.aria-label]="'pagination.options.description' | translate"><i class="fas fa-cog" aria-hidden="true"></i></button>
<div id="paginationControlsDropdownMenu" aria-labelledby="paginationControls" ngbDropdownMenu>
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
<button class="dropdown-item" *ngFor="let item of pageSizeOptions" (click)="doPageSizeChange(item)"><i [ngClass]="{'invisible': item != (pageSize$|async)}" class="fas fa-check" aria-hidden="true"></i> {{item}} </button>
<ng-container *ngIf="!hideSortOptions">
<h6 class="dropdown-header">{{ 'pagination.sort-direction' | translate}}</h6>
<button class="dropdown-item" *ngFor="let direction of (sortDirections | dsKeys)" (click)="doSortDirectionChange(direction.value)"><i [ngClass]="{'invisible': direction.value !== (sortDirection$ |async)}" class="fas fa-check" aria-hidden="true"></i> {{'sorting.' + direction.key | translate}} </button>
</ng-container>
</div>
<button class="btn btn-secondary" id="paginationControls" ngbDropdownToggle [title]="'pagination.options.description' | translate" [attr.aria-label]="'pagination.options.description' | translate" aria-haspopup="true" aria-expanded="false"><i class="fas fa-cog" aria-hidden="true"></i></button>
<ul id="paginationControlsDropdownMenu" aria-labelledby="paginationControls" role="menu" ngbDropdownMenu>
<li role="menuitem">
<span class="dropdown-header" id="pagination-control_results-per-page" role="heading">{{ 'pagination.results-per-page' | translate}}</span>
<ul aria-labelledby="pagination-control_results-per-page" class="list-unstyled" role="listbox">
<li *ngFor="let item of pageSizeOptions" role="option" [attr.aria-selected]="item === (pageSize$ | async)">
<button (click)="doPageSizeChange(item)" class="dropdown-item">
<i [ngClass]="{'invisible': item !== (pageSize$ | async) }" class="fas fa-check" aria-hidden="true"></i> {{item}}
</button>
</li>
</ul>
</li>
<li *ngIf="!hideSortOptions" role="menuitem">
<span class="dropdown-header" id="pagination-control_sort-direction" role="heading">{{ 'pagination.sort-direction' | translate}}</span>
<ul aria-labelledby="pagination-control_sort-direction" class="list-unstyled" role="listbox">
<li *ngFor="let direction of (sortDirections | dsKeys)" [attr.aria-selected]="direction.value === (sortDirection$ | async)" role="option">
<button class="dropdown-item" (click)="doSortDirectionChange(direction.value)">
<i [ngClass]="{'invisible': direction.value !== (sortDirection$ |async)}" class="fas fa-check" aria-hidden="true"></i> {{'sorting.' + direction.key | translate}}
</button>
</li>
</ul>
</li>
</ul>
</div>
<ds-rss></ds-rss>
</div>

View File

@@ -167,7 +167,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
/**
* Number of items per page.
*/
public pageSize$;
public pageSize$: Observable<number>;
/**
* Declare SortDirection enumeration to use it in the template
@@ -188,7 +188,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
/**
* Name of the field that's used to sort by
*/
public sortField$;
public sortField$: Observable<string>;
public defaultSortField = 'name';
/**

View File

@@ -5,8 +5,8 @@
[id]="entry.id"
[ngModel]="entry.checked"
(ngModelChange)="this.toggleCheckbox.emit($event);">
<label class="custom-control-label" [for]="entry.id"
[attr.aria-label]="(entry.checked ? 'resource-policies.table.headers.deselect' : 'resource-policies.table.headers.select') | translate">
<label class="custom-control-label" [for]="entry.id">
<span class="sr-only">{{(entry.checked ? 'resource-policies.table.headers.deselect' : 'resource-policies.table.headers.select') | translate}}</span>
</label>
</div>
</td>
@@ -30,12 +30,12 @@
<button class="btn btn-outline-primary btn-sm"
[title]="'resource-policies.table.headers.edit.policy' | translate"
(click)="redirectToResourcePolicyEditPage()">
<i class="fas fa-edit fa-fw"></i>
<i class="fas fa-edit fa-fw" aria-hidden="true"></i>
</button>
<button *ngIf="(groupName$ | async) !== undefined" class="btn btn-outline-primary btn-sm"
[title]="'resource-policies.table.headers.edit.group' | translate"
(click)="redirectToGroupEditPage()">
<i class="fas fa-users fa-fw"></i>
<i class="fas fa-users fa-fw" aria-hidden="true"></i>
</button>
</div>
</td>

View File

@@ -17,10 +17,10 @@
[title]="'resource-policies.delete.btn.title' | translate"
(click)="deleteSelectedResourcePolicies()">
<span *ngIf="(isProcessingDelete() | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}
<i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'submission.workflow.tasks.generic.processing' | translate}}
</span>
<span *ngIf="!(isProcessingDelete() | async)">
<i class='fas fa-trash-alt fa-fw'></i>
<i class='fas fa-trash-alt fa-fw' aria-hidden="true"></i>
{{'resource-policies.delete.btn' | translate}}
</span>
</button>
@@ -28,7 +28,7 @@
[disabled]="(isProcessingDelete() | async)"
[title]="'resource-policies.add.for.' + resourceType | translate"
(click)="redirectToResourcePolicyCreatePage()">
<i class='fas fa-plus'></i>
<i class='fas fa-plus' aria-hidden="true"></i>
{{'resource-policies.add.button' | translate}}
</button>
</div>
@@ -42,8 +42,8 @@
class="custom-control-input"
[id]="'selectAll_' + resourceUUID"
(change)="selectAllCheckbox($event)">
<label class="custom-control-label" [for]="'selectAll_' + resourceUUID"
[attr.aria-label]="(selectAllBtn.checked ? 'resource-policies.table.headers.deselect-all' : 'resource-policies.table.headers.select-all') | translate">
<label class="custom-control-label" [for]="'selectAll_' + resourceUUID">
<span class="sr-only">{{(selectAllBtn.checked ? 'resource-policies.table.headers.deselect-all' : 'resource-policies.table.headers.select-all') | translate}}</span>
</label>
</div>
</th>

View File

@@ -26,10 +26,9 @@
/>
</label>
</div>
<label class="d-none">
<input type="submit" class="d-none"/>
<span>{{'search.filters.search.submit' | translate}}</span>
</label>
<button class="sr-only" type="submit">
{{'search.filters.search.submit' | translate}}
</button>
</form>
<ng-container *ngIf="shouldShowSlider()">

View File

@@ -82,7 +82,7 @@ export class SubscriptionViewComponent {
deleteSubscriptionPopup(subscription: Subscription) {
if (hasValue(subscription.id)) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = this.dso;
modalRef.componentInstance.name = this.dsoNameService.getName(this.dso);
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-subscription.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-subscription.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-subscription.cancel';

View File

@@ -23,7 +23,7 @@
<i class="fas fa-upload" aria-hidden="true"></i>
{{dropMsg | translate}}{{'uploader.or' | translate}}
<label for="inputFileUploader-{{uploaderId}}" class="btn btn-link m-0 p-0 ml-1" tabindex="0" (keyup.enter)="$event.stopImmediatePropagation(); fileInput.click()">
<span role="button" [attr.aria-label]="'uploader.browse' | translate">{{'uploader.browse' | translate}}</span>
<span role="button" [attr.aria-label]="ariaLabel | translate">{{'uploader.browse' | translate}}</span>
</label>
<input #fileInput id="inputFileUploader-{{uploaderId}}" class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple tabindex="0" />
</span>

View File

@@ -53,6 +53,11 @@ export class UploaderComponent {
*/
@Input() uploadProperties: UploaderProperties;
/**
* The aria label to describe what kind of files need to be uploaded
*/
@Input() ariaLabel: string;
/**
* The function to call when upload is completed
*/

View File

@@ -32,7 +32,7 @@
</button>
<div ngbDropdownMenu
class="dropdown-menu"
class="dropdown-menu p-0"
id="collectionControlsDropdownMenu"
aria-labelledby="collectionControlsMenuButton">
<ds-themed-collection-dropdown

View File

@@ -1,25 +1,28 @@
<div *ngIf="(isXsOrSm$ | async)" class="input-group mb-2">
<input type="text" class="form-control" (keyup.enter)="(searchString === '')?null:search()" [(ngModel)]="searchString" placeholder="{{'submission.import-external.search.placeholder' |translate}}" aria-label="" aria-describedby="">
<input type="text" class="form-control" (keyup.enter)="(searchString === '')?null:search()" [(ngModel)]="searchString" placeholder="{{'submission.import-external.search.placeholder' | translate}}" [attr.aria-label]="'submission.import-external.search.placeholder' | translate">
</div>
<div class="input-group mb-5">
<input *ngIf="!(isXsOrSm$ | async)" type="text" class="form-control" (keyup.enter)="(searchString === '')?null:search()" [(ngModel)]="searchString" placeholder="{{'submission.import-external.search.placeholder' |translate}}" aria-label="" aria-describedby="">
<div [ngClass]="{'input-group-append': !(isXsOrSm$ | async)}" ngbDropdown role="group" aria-label="">
<button class="btn btn-outline-secondary w-fx" title="{{'submission.import-external.search.source.hint' |translate}}" ngbDropdownToggle>{{'submission.import-external.source.' + selectedElement?.name | translate}}</button>
<div ngbDropdownMenu class="dropdown-menu scrollable-dropdown-menu w-100"
<input *ngIf="!(isXsOrSm$ | async)" type="text" class="form-control" (keyup.enter)="(searchString === '')?null:search()" [(ngModel)]="searchString" placeholder="{{'submission.import-external.search.placeholder' | translate}}" [attr.aria-label]="'submission.import-external.search.placeholder' | translate">
<div [ngClass]="{'input-group-append': !(isXsOrSm$ | async)}" ngbDropdown role="group">
<button [attr.aria-label]="'submission.import-external.search.source.hint' |translate"
class="btn btn-outline-secondary w-fx"
title="{{'submission.import-external.search.source.hint' |translate}}"
ngbDropdownToggle>
{{'submission.import-external.source.' + selectedElement?.name | translate}}
</button>
<ul ngbDropdownMenu class="dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
aria-labelledby="scrollableDropdownMenuButton">
<div class="scrollable-menu"
aria-labelledby="scrollableDropdownMenuButton"
aria-expanded="false">
<li class="scrollable-menu"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
(scrolled)="onScroll()"
[scrollWindow]="false">
<button ngbDropdownItem class="dropdown-item text-truncate" title="{{'submission.import-external.source.' + source?.name | translate}}" (click)="makeSourceSelection(source)" *ngFor="let source of sourceList">{{'submission.import-external.source.' + source?.name | translate}}</button>
<button ngbDropdownItem class="dropdown-item text-truncate" (click)="makeSourceSelection(source)" *ngFor="let source of sourceList">{{'submission.import-external.source.' + source?.name | translate}}</button>
<div ngbDropdownItem class="scrollable-dropdown-loading text-center" *ngIf="sourceListLoading"><p>{{'submission.import-external.source.loading' | translate}}</p></div>
</div>
</div>
<button type="button" class="btn btn-primary" [title]="(searchString === '')?('submission.import-external.search.button.hint' | translate):('submission.import-external.search.button' | translate)" [disabled]="searchString === ''" (click)="search()">{{'submission.import-external.search.button' | translate}}</button>
</li>
</ul>
<button type="button" class="btn btn-primary" [title]="(searchString === '')?('submission.import-external.search.button.hint' | translate):''" [disabled]="searchString === ''" (click)="search()">{{'submission.import-external.search.button' | translate}}</button>
</div>
</div>

View File

@@ -128,6 +128,8 @@
"admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.selected": "Selected bitstream formats",
"admin.registries.bitstream-formats.table.id": "ID",
"admin.registries.bitstream-formats.table.return": "Back",
@@ -142,7 +144,9 @@
"admin.registries.bitstream-formats.title": "Bitstream Format Registry",
"admin.registries.bitstream-formats.select": "Select bitstream format",
"admin.registries.bitstream-formats.select": "Select",
"admin.registries.bitstream-formats.deselect": "Deselect",
"admin.registries.metadata.breadcrumbs": "Metadata registry",
@@ -160,8 +164,14 @@
"admin.registries.metadata.schemas.no-items": "No metadata schemas to show.",
"admin.registries.metadata.schemas.select": "Select",
"admin.registries.metadata.schemas.deselect": "Deselect",
"admin.registries.metadata.schemas.table.delete": "Delete selected",
"admin.registries.metadata.schemas.table.selected": "Selected schemas",
"admin.registries.metadata.schemas.table.id": "ID",
"admin.registries.metadata.schemas.table.name": "Name",
@@ -174,6 +184,10 @@
"admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".",
"admin.registries.schema.fields.select": "Select",
"admin.registries.schema.fields.deselect": "Deselect",
"admin.registries.schema.fields.head": "Schema metadata fields",
"admin.registries.schema.fields.no-items": "No metadata fields to show.",
@@ -182,6 +196,8 @@
"admin.registries.schema.fields.table.field": "Field",
"admin.registries.schema.fields.table.selected": "Selected metadata fields",
"admin.registries.schema.fields.table.id": "ID",
"admin.registries.schema.fields.table.scopenote": "Scope Note",
@@ -892,6 +908,8 @@
"claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow",
"collection.browse.logo": "Browse for a collection logo",
"collection.create.head": "Create a Collection",
"collection.create.notifications.success": "Successfully created the Collection",
@@ -1088,6 +1106,8 @@
"collection.listelement.badge": "Collection",
"collection.logo": "Collection logo",
"collection.page.browse.recent.head": "Recent Submissions",
"collection.page.browse.recent.empty": "No items to show",
@@ -1104,6 +1124,8 @@
"collection.select.empty": "No collections to show",
"collection.select.table.selected": "Selected collections",
"collection.select.table.select": "Select collection",
"collection.select.table.deselect": "Deselect collection",
@@ -1168,6 +1190,12 @@
"communityList.showMore": "Show More",
"communityList.expand": "Expand {{ name }}",
"communityList.collapse": "Collapse {{ name }}",
"community.browse.logo": "Browse for a community logo",
"community.create.head": "Create a Community",
"community.create.notifications.success": "Successfully created the Community",
@@ -1244,6 +1272,8 @@
"community.listelement.badge": "Community",
"community.logo": "Community logo",
"comcol-role.edit.no-group": "None",
"comcol-role.edit.create": "Create",
@@ -1690,7 +1720,7 @@
"forgot-password.form.label.passwordrepeat": "Retype to confirm",
"forgot-password.form.error.empty-password": "Please enter a password in the box below.",
"forgot-password.form.error.empty-password": "Please enter a password in the boxes above.",
"forgot-password.form.error.matching-passwords": "The passwords do not match.",
@@ -1914,7 +1944,7 @@
"info.feedback.page-label": "Page",
"info.feedback.page_help": "Tha page related to your feedback",
"info.feedback.page_help": "The page related to your feedback",
"item.alerts.private": "This item is non-discoverable",
@@ -2142,6 +2172,8 @@
"item.edit.metadata.headers.value": "Value",
"item.edit.metadata.metadatafield": "Edit field",
"item.edit.metadata.metadatafield.error": "An error occurred validating the metadata field",
"item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field",
@@ -2534,6 +2566,8 @@
"item.select.empty": "No items to show",
"item.select.table.selected": "Selected items",
"item.select.table.select": "Select item",
"item.select.table.deselect": "Deselect item",
@@ -2664,6 +2698,8 @@
"itemtemplate.edit.metadata.headers.value": "Value",
"itemtemplate.edit.metadata.metadatafield": "Edit field",
"itemtemplate.edit.metadata.metadatafield.error": "An error occurred validating the metadata field",
"itemtemplate.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field",
@@ -3458,8 +3494,6 @@
"profile.special.groups.head": "Authorization special groups you belong to",
"profile.head": "Update Profile",
"profile.metadata.form.error.firstname.required": "First Name is required",
"profile.metadata.form.error.lastname.required": "Last Name is required",
@@ -4570,6 +4604,8 @@
"submission.sections.general.no-collection": "No collection found",
"submission.sections.general.no-entity": "No entity types found",
"submission.sections.general.no-sections": "No options available",
"submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.",

View File

@@ -106,4 +106,7 @@
--ds-dso-edit-lang-width: 90px;
--ds-dso-edit-actions-width: 173px;
--ds-dso-edit-virtual-tooltip-min-width: 300px;
--ds-comcol-logo-max-width: 500px;
--ds-comcol-logo-max-height: 500px;
}