Skip to content

Commit

Permalink
Merge branch 'main' into DURACOM-191
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/app/shared/confirmation-modal/confirmation-modal.component.ts
#	src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts
#	src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts
  • Loading branch information
Andrea Barbasso committed Jan 24, 2024
2 parents b53f5a1 + c02b46c commit 1afec99
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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';
import { TranslateModule } from '@ngx-translate/core';
import { NgIf } from '@angular/common';

Expand All @@ -22,7 +20,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
Expand All @@ -32,7 +30,6 @@ export class ConfirmationModalComponent {

constructor(
protected activeModal: NgbActiveModal,
public dsoNameService: DSONameService,
) {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { BATCH_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
Expand All @@ -22,6 +22,7 @@ import { AuthorizationDataService } from '../../../../core/data/feature-authoriz
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
import { DSOSelectorComponent } from '../../dso-selector/dso-selector.component';
import { NgIf } from '@angular/common';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';

/**
* Component to wrap a list of existing dso's inside a modal
Expand All @@ -42,6 +43,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);
}
Expand All @@ -53,7 +55,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';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
Expand All @@ -23,6 +23,7 @@ import { AuthorizationDataService } from '../../../../core/data/feature-authoriz
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
import { DSOSelectorComponent } from '../../dso-selector/dso-selector.component';
import { NgIf } from '@angular/common';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';

/**
* Component to wrap a list of existing dso's inside a modal
Expand All @@ -43,6 +44,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);
}
Expand All @@ -54,7 +56,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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild
} 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, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';

Expand All @@ -12,10 +21,7 @@ import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { isEmpty } from '../../../../../empty.util';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
import {
PaginatedList,
buildPaginatedList
} from '../../../../../../core/data/paginated-list.model';
import { buildPaginatedList, PaginatedList } from '../../../../../../core/data/paginated-list.model';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
Expand All @@ -40,6 +46,8 @@ import { TranslateModule } from '@ngx-translate/core';
standalone: true
})
export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
@ViewChild('dropdownMenu', { read: ElementRef }) dropdownMenu: ElementRef;

@Input() bindId = true;
@Input() group: UntypedFormGroup;
@Input() model: DynamicScrollableDropdownModel;
Expand All @@ -52,6 +60,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,
Expand All @@ -66,32 +77,26 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
*/
ngOnInit() {
this.updatePageInfo(this.model.maxOptions, 1);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
this.loadOptions();
}

loadOptions() {
this.loading = true;
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, 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.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.cdr.detectChanges();
});

this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged())
.subscribe((value) => {
this.setCurrentValue(value);
});
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();
});
}

/**
Expand All @@ -106,10 +111,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
Expand All @@ -118,13 +143,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();
if (sdRef.isOpen()) {
this.onSelect(this.optionsList[this.selectedIndex]);
sdRef.close();
} else {
sdRef.open();
}
} else if (keyName === 'ArrowDown' || keyName === 'ArrowUp') {
this.openDropdown(sdRef);
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);
}

/**
Expand All @@ -139,7 +205,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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,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';
Expand Down

0 comments on commit 1afec99

Please sign in to comment.