Merge branch 'DSpace:main' into StatusPageAccessibility

This commit is contained in:
Andrea-Guevara
2024-10-01 12:13:09 -03:00
committed by GitHub
111 changed files with 25250 additions and 12700 deletions

View File

@@ -2,7 +2,7 @@
* List of services statuses
*/
export enum LdnServiceStatus {
UNKOWN,
UNKNOWN,
DISABLED,
ENABLED,
}

View File

@@ -190,12 +190,12 @@ describe('BitstreamFormatsComponent', () => {
describe('isSelected', () => {
beforeEach(waitForAsync(initAsync));
beforeEach(initBeforeEach);
it('should return an observable of true if the provided bistream is in the list returned by the service', () => {
it('should return an observable of true if the provided bitstream is in the list returned by the service', () => {
const result = comp.isSelected(bitstreamFormat1);
expect(result).toBeObservable(cold('b', { b: true }));
});
it('should return an observable of false if the provided bistream is not in the list returned by the service', () => {
it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => {
const format = new BitstreamFormat();
format.uuid = 'new';

View File

@@ -118,7 +118,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
}
/**
* Deselects all selecetd bitstream formats
* Deselects all selected bitstream formats
*/
deselectAll() {
this.bitstreamFormatService.deselectAllBitstreamFormats();

View File

@@ -154,12 +154,12 @@ export class FormatFormComponent implements OnInit {
(fieldModel: DynamicFormControlModel) => {
if (fieldModel.name === 'extensions') {
if (hasValue(this.bitstreamFormat.extensions)) {
const extenstions = this.bitstreamFormat.extensions;
const extensions = this.bitstreamFormat.extensions;
const formArray = (fieldModel as DynamicFormArrayModel);
for (let i = 0; i < extenstions.length; i++) {
for (let i = 0; i < extensions.length; i++) {
formArray.insertGroup(i).group[0] = new DynamicInputModel({
id: `extension-${i}`,
value: extenstions[i],
value: extensions[i],
}, this.arrayInputElementLayout);
}
}
@@ -172,7 +172,7 @@ export class FormatFormComponent implements OnInit {
}
/**
* Creates an updated bistream format based on the current values in the form
* Creates an updated bitstream format based on the current values in the form
* Emits the updated bitstream format trouhg the updatedFormat emitter
*/
onSubmit() {

View File

@@ -154,7 +154,7 @@ export class BrowseByTaxonomyComponent implements OnInit, OnChanges, OnDestroy {
this.facetType = browseDefinition.facetType;
this.vocabularyName = browseDefinition.vocabulary;
this.vocabularyOptions = { name: this.vocabularyName, closed: true };
this.description = this.translate.instant(`browse.metadata.${this.vocabularyName}.tree.descrption`);
this.description = this.translate.instant(`browse.metadata.${this.vocabularyName}.tree.description`);
}));
this.subs.push(this.scope$.subscribe(() => {
this.updateQueryParams();

View File

@@ -84,7 +84,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
}
/**
* Cheking if the navigation is done and if so, initialize the collection's item template,
* Checking if the navigation is done and if so, initialize the collection's item template,
* to ensure that the item template is always up to date.
* Check when a NavigationEnd event (URL change) or a Scroll event followed by a NavigationEnd event (refresh event), occurs
*/

View File

@@ -304,7 +304,7 @@ function addDependentsObjectCacheState(state: ObjectCacheState, action: AddDepen
/**
* Remove all dependent request UUIDs from a cached object, used to clear out-of-date depedencies
* Remove all dependent request UUIDs from a cached object, used to clear out-of-date dependencies
*
* @param state the current state
* @param action an AddDependentsObjectCacheAction

View File

@@ -63,12 +63,12 @@ describe('AccessStatusDataService', () => {
/**
* Create an AccessStatusDataService used for testing
* @param reponse$ Supply a RemoteData to be returned by the REST API (optional)
* @param response$ Supply a RemoteData to be returned by the REST API (optional)
*/
function createService(reponse$?: Observable<RemoteData<any>>) {
function createService(response$?: Observable<RemoteData<any>>) {
requestService = getMockRequestService();
let buildResponse$ = reponse$;
if (hasNoValue(reponse$)) {
let buildResponse$ = response$;
if (hasNoValue(response$)) {
buildResponse$ = createSuccessfulRemoteDataObject$({});
}
rdbService = jasmine.createSpyObj('rdbService', {

View File

@@ -138,27 +138,27 @@ describe('BitstreamDataService', () => {
describe('findPrimaryBitstreamByItemAndName', () => {
it('should return primary bitstream', () => {
const exprected$ = cold('(a|)', { a: bitstream1 } );
const expected$ = cold('(a|)', { a: bitstream1 } );
const bundle = Object.assign(new Bundle(), {
primaryBitstream: observableOf(createSuccessfulRemoteDataObject(bitstream1)),
});
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle)));
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$);
});
it('should return null if primary bitstream has not be succeeded ', () => {
const exprected$ = cold('(a|)', { a: null } );
const expected$ = cold('(a|)', { a: null } );
const bundle = Object.assign(new Bundle(), {
primaryBitstream: observableOf(createFailedRemoteDataObject()),
});
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle)));
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$);
});
it('should return EMPTY if nothing where found', () => {
const exprected$ = cold('(|)', {} );
const expected$ = cold('(|)', {} );
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createFailedRemoteDataObject<Bundle>()));
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$);
});
});

View File

@@ -242,7 +242,7 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @return {Observable<Bitstream | null>}
* Return an observable that constains primary bitstream information or null
* Return an observable that contains primary bitstream information or null
*/
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<Bitstream | null> {
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe(

View File

@@ -151,7 +151,7 @@ describe('CollectionDataService', () => {
expect(service.getAuthorizedCollection).toHaveBeenCalledWith(queryString);
});
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollection', () => {
it('should return a RemoteData<PaginatedList<Collection>> for the getAuthorizedCollection', () => {
const result = service.getAuthorizedCollection(queryString);
const expected = cold('a|', {
a: paginatedListRD,
@@ -166,7 +166,7 @@ describe('CollectionDataService', () => {
expect(service.getAuthorizedCollectionByCommunity).toHaveBeenCalledWith(communityId, queryString);
});
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollectionByCommunity', () => {
it('should return a RemoteData<PaginatedList<Collection>> for the getAuthorizedCollectionByCommunity', () => {
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString);
const expected = cold('a|', {
a: paginatedListRD,
@@ -206,12 +206,12 @@ describe('CollectionDataService', () => {
/**
* Create a CollectionDataService used for testing
* @param reponse$ Supply a RemoteData to be returned by the REST API (optional)
* @param response$ Supply a RemoteData to be returned by the REST API (optional)
*/
function createService(reponse$?: Observable<RemoteData<any>>) {
function createService(response$?: Observable<RemoteData<any>>) {
requestService = getMockRequestService();
let buildResponse$ = reponse$;
if (hasNoValue(reponse$)) {
let buildResponse$ = response$;
if (hasNoValue(response$)) {
buildResponse$ = createSuccessfulRemoteDataObject$({});
}
rdbService = jasmine.createSpyObj('rdbService', {

View File

@@ -20,7 +20,7 @@ export class LinkHeadService {
/**
* Method to create a Link tag in the HEAD of the html.
* @param tag LinkDefition is the paramaters to define a link tag.
* @param tag LinkDefition is the parameters to define a link tag.
* @returns Link tag that was created
*/
addTag(tag: LinkDefinition) {

View File

@@ -33,7 +33,7 @@ const httpAgent = new HttpAgent(agentOptions);
const httpsAgent = new HttpsAgent(agentOptions);
/**
* Contructs the XMLHttpRequest instances used for all HttpClient requests.
* Constructs the XMLHttpRequest instances used for all HttpClient requests.
* Emulated by https://github.com/pwnall/node-xhr2 on the server.
* This class overrides the built-in Angular implementation to set additional configuration.
*

View File

@@ -3,7 +3,7 @@
*/
export class MetadataConfig {
/**
* A unique indentifier
* A unique identifier
*/
id: string;

View File

@@ -57,7 +57,7 @@ import { SearchConfigurationService } from './search-configuration.service';
* - Overrides {@link BaseDataService.addEmbedParams} in order to make it public
*
* Doesn't use any of the service's dependencies, they are initialized as undefined
* Therefore, equest/response handling methods won't work even though they're defined
* Therefore, request/response handling methods won't work even though they're defined
*/
class SearchDataService extends BaseDataService<any> {
constructor() {

View File

@@ -68,7 +68,7 @@ export class SubmissionObjectDataService {
environment.cache.msToLive.default,
now,
RequestEntryState.Error,
'The request couldn\'t be sent. Unable to determine the type of submission object',
'The request could not be sent. Unable to determine the type of submission object',
undefined,
400,
));

View File

@@ -244,7 +244,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
/**
* Submit the current changes to the form by retrieving json PATCH operations from the form and sending it to the
* DSpaceObject's data-service
* Display notificiations and reset the form afterwards if successful
* Display notifications and reset the form afterwards if successful
*/
submit(): void {
this.saving$.next(true);

View File

@@ -23,7 +23,7 @@
[label]="'orgunit.page.city'">
</ds-generic-item-page-field>
<ds-generic-item-page-field [item]="object"
[fields]="['organization.adress.addressCountry']"
[fields]="['organization.address.addressCountry']"
[label]="'orgunit.page.country'">
</ds-generic-item-page-field>
<ds-generic-item-page-field [item]="object"

View File

@@ -23,7 +23,7 @@ const mockItem: Item = Object.assign(new Item(), {
value: 'New York',
},
],
'organization.adress.addressCountry': [
'organization.address.addressCountry': [
{
language: 'en_US',
value: 'USA',

View File

@@ -50,7 +50,7 @@ import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
* Performs the initialization of the app.
*
* Should be extended to implement server- & browser-specific functionality.
* Initialization steps shared between the server and brower implementations
* Initialization steps shared between the server and browser implementations
* can be included in this class.
*
* Note that the service cannot (indirectly) depend on injection tokens that are only available _after_ APP_INITIALIZER.
@@ -134,7 +134,7 @@ export abstract class InitService {
protected static resolveAppConfig(
transferState: TransferState,
): void {
// overriden in subclasses if applicable
// overridden in subclasses if applicable
}
/**

View File

@@ -253,7 +253,7 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
}
/**
* Retrieves the link to the bistream download page
* Retrieves the link to the bitstream download page
*/
getBitstreamLink() {
return [getBitstreamDownloadRoute(this.bitstream)];

View File

@@ -375,7 +375,7 @@ describe('EditRelationshipListComponent', () => {
describe('changes managment for add buttons', () => {
describe('changes management for add buttons', () => {
it('should show enabled add buttons', () => {
const element = de.query(By.css('.btn-success'));

View File

@@ -45,7 +45,7 @@ export class MetadataUriValuesComponent extends MetadataValuesComponent {
@Input() mdValues: MetadataValue[];
/**
* The seperator used to split the metadata values (can contain HTML)
* The separator used to split the metadata values (can contain HTML)
*/
@Input() separator: string;

View File

@@ -51,7 +51,7 @@ export class MetadataValuesComponent implements OnChanges {
@Input() mdValues: MetadataValue[];
/**
* The seperator used to split the metadata values (can contain HTML)
* The separator used to split the metadata values (can contain HTML)
*/
@Input() separator: string;

View File

@@ -17,7 +17,7 @@ import { AuthService } from '../../../core/auth/auth.service';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
/**
* This componenet render an image gallery for the image viewer
* This component render an image gallery for the image viewer
*/
@Component({
selector: 'ds-base-media-viewer-image',

View File

@@ -34,13 +34,13 @@ export class ItemPageCcLicenseFieldComponent implements OnInit {
@Input() variant?: 'small' | 'full' = 'small';
/**
* Filed name containing the CC license URI, as configured in the back-end, in the 'dspace.cfg' file, propertie
* Filed name containing the CC license URI, as configured in the back-end, in the 'dspace.cfg' file, property
* 'cc.license.uri'
*/
@Input() ccLicenseUriField? = 'dc.rights.uri';
/**
* Filed name containing the CC license name, as configured in the back-end, in the 'dspace.cfg' file, propertie
* Filed name containing the CC license name, as configured in the back-end, in the 'dspace.cfg' file, property
* 'cc.license.name'
*/
@Input() ccLicenseNameField? = 'dc.rights';

View File

@@ -1,7 +1,9 @@
<ng-container *ngIf="relationTypes.length > 1">
<ul ngbNav #tabs="ngbNav" [destroyOnHide]="true" [activeId]="activeTab$ | async" (navChange)="onTabChange($event)" class="nav-tabs">
<li *ngFor="let relationType of relationTypes" [ngbNavItem]="relationType.filter" rel="presentation">
<a ngbNavLink>{{'item.page.relationships.' + relationType.label | translate}}</a>
<ul ngbNav #tabs="ngbNav" [destroyOnHide]="true" [activeId]="activeTab$ | async" (navChange)="onTabChange($event)" class="nav-tabs" role="tablist">
<li *ngFor="let relationType of relationTypes" [ngbNavItem]="relationType.filter" role="presentation">
<a ngbNavLink role="tab">
{{'item.page.relationships.' + relationType.label | translate}}
</a>
<ng-template ngbNavContent>
<div class="mt-4">
<ds-related-entities-search [item]="item"

View File

@@ -1,16 +1,16 @@
<div *ngVar="(versionsRD$ | async)?.payload as versions">
<div *ngVar="(versionRD$ | async)?.payload as itemVersion">
<div class="mb-2" *ngIf="versions?.page?.length > 0 || displayWhenEmpty">
<div *ngIf="(versionsDTO$ | async) as versionsDTO; else noItemVersion">
<div *ngIf="(versionRD$ | async)?.payload as itemVersion">
<div class="mb-2" *ngIf="versionsDTO.versionDTOs.length > 0 || displayWhenEmpty">
<h2 *ngIf="displayTitle" class="h4">{{"item.version.history.head" | translate}}</h2>
<ds-alert [type]="AlertTypeEnum.Info" *ngIf="itemVersion">
<ds-alert [type]="AlertTypeEnum.Info">
{{ "item.version.history.selected.alert" | translate : {version: itemVersion.version} }}
</ds-alert>
<ds-pagination *ngIf="versions?.page?.length > 0"
<ds-pagination *ngIf="versionsDTO.versionDTOs.length > 0"
(paginationChange)="onPageChange()"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
[paginationOptions]="options"
[collectionSize]="versions?.totalElements"
[collectionSize]="versionsDTO.totalElements"
[retainScrollPosition]="true">
<table class="table table-striped table-bordered align-middle my-2">
<thead>
@@ -22,10 +22,10 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
<tr *ngFor="let versionDTO of versionsDTO.versionDTOs" [id]="'version-row-' + versionDTO.version.id">
<td class="version-row-element-version">
<ds-item-versions-row-element-version [hasDraftVersion]="hasDraftVersion$ | async"
[version]="version"
[version]="versionDTO.version"
[item]="item" [displayActions]="displayActions"
[itemVersion]="itemVersion"
[versionBeingEditedNumber]="versionBeingEditedNumber"
@@ -33,15 +33,15 @@
></ds-item-versions-row-element-version>
</td>
<td class="version-row-element-editor" *ngIf="(showSubmitter$ | async)">
{{version?.submitterName}}
{{versionDTO.version.submitterName}}
</td>
<td class="version-row-element-date">
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}
{{versionDTO.version.created | date : 'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="version-row-element-summary">
<div class="float-left">
<ng-container *ngIf="isThisBeingEdited(version); then editSummary else showSummary"></ng-container>
<ng-template #showSummary>{{version?.summary}}</ng-template>
<ng-container *ngIf="isThisBeingEdited(versionDTO.version); then editSummary else showSummary"></ng-container>
<ng-template #showSummary>{{versionDTO.version.summary}}</ng-template>
<ng-template #editSummary>
<input [attr.aria-label]="'item.version.history.table.action.editSummary' | translate"
[(ngModel)]="versionBeingEditedSummary" (keyup.enter)="onSummarySubmit()"
@@ -49,43 +49,44 @@
</ng-template>
</div>
<div class="float-right btn-group edit-field space-children-mr" *ngIf="displayActions">
<!--DISCARD EDIT -->
<ng-container *ngIf="(canEditVersion$(version) | async) && isThisBeingEdited(version)">
<button class="btn btn-sm"
[ngClass]="isThisBeingEdited(version) ? 'btn-outline-warning' : 'btn-outline-primary'"
<div class="float-right btn-group edit-field space-children-mr" *ngIf="displayActions && versionDTO.canEditVersion | async">
<ng-container *ngIf="isThisBeingEdited(versionDTO.version); else notThisBeingEdited">
<!--DISCARD EDIT-->
<button class="btn btn-sm btn-outline-warning"
(click)="disableVersionEditing()"
title="{{'item.version.history.table.action.discardSummary' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</ng-container>
<!--EDIT / SAVE-->
<ng-container *ngIf="canEditVersion$(version) | async">
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
*ngIf="!isThisBeingEdited(version)"
[disabled]="isAnyBeingEdited()"
(click)="enableVersionEditing(version)"
title="{{'item.version.history.table.action.editSummary' | translate}}">
<i class="fas fa-edit fa-fw"></i>
</button>
<!--SAVE-->
<button class="btn btn-outline-success btn-sm"
*ngIf="isThisBeingEdited(version)"
(click)="onSummarySubmit()"
title="{{'item.version.history.table.action.saveSummary' | translate}}">
<i class="fas fa-check fa-fw"></i>
</button>
</ng-container>
<ng-template #notThisBeingEdited>
<!--EDIT-->
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
[disabled]="isAnyBeingEdited()"
(click)="enableVersionEditing(versionDTO.version)"
title="{{'item.version.history.table.action.editSummary' | translate}}">
<i class="fas fa-edit fa-fw"></i>
</button>
</ng-template>
</div>
</td>
</tr>
</tbody>
</table>
<div>*&nbsp;{{"item.version.history.selected" | translate}}</div>
</ds-pagination>
<ds-alert *ngIf="!itemVersion || versions?.page?.length === 0" [content]="'item.version.history.empty'"
[type]="AlertTypeEnum.Info"></ds-alert>
</div>
</div>
</div>
<ng-template #noItemVersion>
<ds-alert *ngIf="displayWhenEmpty"
[content]="'item.version.history.empty'"
[type]="AlertTypeEnum.Info">
</ds-alert>
</ng-template>

View File

@@ -12,13 +12,11 @@ import {
OnInit,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
BehaviorSubject,
combineLatest,
Observable,
Subscription,
@@ -41,7 +39,6 @@ import { PaginationService } from '../../core/pagination/pagination.service';
import { Item } from '../../core/shared/item.model';
import {
getAllSucceededRemoteData,
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
@@ -60,16 +57,35 @@ import { PaginationComponent } from '../../shared/pagination/pagination.componen
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { VarDirective } from '../../shared/utils/var.directive';
import { getItemPageRoute } from '../item-page-routing-paths';
import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version/item-versions-row-element-version.component';
interface VersionsDTO {
totalElements: number;
versionDTOs: VersionDTO[];
}
interface VersionDTO {
version: Version;
canEditVersion: Observable<boolean>;
}
@Component({
selector: 'ds-item-versions',
templateUrl: './item-versions.component.html',
styleUrls: ['./item-versions.component.scss'],
standalone: true,
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent],
imports: [
AlertComponent,
AsyncPipe,
DatePipe,
FormsModule,
ItemVersionsRowElementVersionComponent,
NgClass,
NgFor,
NgIf,
PaginationComponent,
TranslateModule,
],
})
/**
@@ -128,13 +144,7 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
/**
* The version history's list of versions
*/
versionsRD$: BehaviorSubject<RemoteData<PaginatedList<Version>>> = new BehaviorSubject<RemoteData<PaginatedList<Version>>>(null);
/**
* Verify if the list of versions has at least one e-person to display
* Used to hide the "Editor" column when no e-persons are present to display
*/
hasEpersons$: Observable<boolean>;
versionsDTO$: Observable<VersionsDTO>;
/**
* Verify if there is an inprogress submission in the version history
@@ -162,15 +172,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
pageSize: this.pageSize,
});
/**
* The routes to the versions their item pages
* Key: Item ID
* Value: Route to item page
*/
itemPageRoutes$: Observable<{
[itemId: string]: string
}>;
/**
* The number of the version whose summary is currently being edited
*/
@@ -186,9 +187,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
*/
versionBeingEditedSummary: string;
canCreateVersion$: Observable<boolean>;
createVersionTitle$: Observable<string>;
constructor(private versionHistoryService: VersionHistoryDataService,
private versionService: VersionDataService,
private paginationService: PaginationService,
@@ -257,8 +255,7 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
this.notificationsService.warning(null, this.translateService.get(failureMessageKey, { 'version': this.versionBeingEditedNumber }));
}
this.disableVersionEditing();
},
);
});
}
/**
@@ -305,16 +302,22 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
*/
getAllVersions(versionHistory$: Observable<VersionHistory>): void {
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
combineLatest([versionHistory$, currentPagination]).pipe(
this.versionsDTO$ = combineLatest([versionHistory$, currentPagination]).pipe(
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => {
return this.versionHistoryService.getVersions(versionHistory.id,
new PaginatedSearchOptions({ pagination: Object.assign({}, options, { currentPage: options.currentPage }) }),
false, true, followLink('item'), followLink('eperson'));
}),
getFirstCompletedRemoteData(),
).subscribe((res: RemoteData<PaginatedList<Version>>) => {
this.versionsRD$.next(res);
});
getRemoteDataPayload(),
map((versions: PaginatedList<Version>) => ({
totalElements: versions.totalElements,
versionDTOs: (versions?.page ?? []).map((version: Version) => ({
version: version,
canEditVersion: this.canEditVersion$(version),
})),
})),
);
}
/**
@@ -348,22 +351,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
);
this.getAllVersions(this.versionHistory$);
this.hasEpersons$ = this.versionsRD$.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
hasValueOperator(),
map((versions: PaginatedList<Version>) => versions.page.filter((version: Version) => version.eperson !== undefined).length > 0),
startWith(false),
);
this.itemPageRoutes$ = this.versionsRD$.pipe(
getAllSucceededRemoteDataPayload(),
switchMap((versions) => combineLatest(versions.page.map((version) => version.item.pipe(getAllSucceededRemoteDataPayload())))),
map((versions) => {
const itemPageRoutes = {};
versions.forEach((item) => itemPageRoutes[item.uuid] = getItemPageRoute(item));
return itemPageRoutes;
}),
);
}
}
@@ -380,3 +367,4 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
}
}

View File

@@ -120,7 +120,7 @@ export class MyDSpaceNewSubmissionDropdownComponent implements OnInit, OnDestroy
}
/**
* Method called on clicking the button "New Submition", It opens a dialog for
* Method called on clicking the button "New Submission", It opens a dialog for
* select a collection.
*/
openDialog(entity: ItemType) {

View File

@@ -140,7 +140,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
} else {
const modalRef = this.modalService.open(CollectionSelectorComponent);
// When the dialog are closes its takes the collection selected and
// uploads choosed file after adds owningCollection parameter
// uploads chosen file after adds owningCollection parameter
modalRef.result.then( (result) => {
uploader.onBuildItemForm = (fileItem: any, form: any) => {
form.append('owningCollection', result.uuid);

View File

@@ -15,8 +15,7 @@
[attr.aria-expanded]="isActive"
[attr.aria-controls]="expandableNavbarSectionId(section.id)"
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
[class.disabled]="section.model?.disabled"
id="browseDropdown">
[class.disabled]="section.model?.disabled">
<span class="flex-fill">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>

View File

@@ -10,7 +10,7 @@
/** Mobile menu styling **/
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
.navbar {
width: 100%;
width: 100vw;
background-color: var(--bs-white);
position: absolute;
overflow: hidden;

View File

@@ -335,7 +335,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
}
/**
* Performs the choosen action calling the REST service.
* Performs the chosen action calling the REST service.
*
* @param {string} action
* the action (can be: ACCEPTED, REJECTED, DISCARDED, PENDING)
@@ -454,7 +454,7 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
}
/**
* Dispatch the Quality Assurance events retrival.
* Dispatch the Quality Assurance events retrieval.
*/
public getQualityAssuranceEvents(): Observable<QualityAssuranceEventData[]> {
return this.paginationService.getFindListOptions(this.paginationConfig.id, this.defaultConfig).pipe(

View File

@@ -201,7 +201,7 @@ export class ProjectEntryImportModalComponent implements OnInit, OnDestroy {
private selectService: SelectableListService) { }
/**
* Component intitialization.
* Component initialization.
*/
public ngOnInit(): void {
this.pagination = Object.assign(new PaginationComponentOptions(), { id: 'notifications-project-bound', pageSize: this.pageSize });

View File

@@ -125,7 +125,7 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
}
/**
* Dispatch the Quality Assurance source retrival.
* Dispatch the Quality Assurance source retrieval.
*/
public getQualityAssuranceSource(): void {
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(

View File

@@ -86,13 +86,13 @@ export class QualityAssuranceTopicsComponent implements OnInit, OnDestroy, After
protected subs: Subscription[] = [];
/**
* This property represents a sourceId which is used to retrive a topic
* This property represents a sourceId which is used to retrieve a topic
* @type {string}
*/
public sourceId: string;
/**
* This property represents a targetId (item-id) which is used to retrive a topic
* This property represents a targetId (item-id) which is used to retrieve a topic
* @type {string}
*/
public targetId: string;
@@ -171,7 +171,7 @@ export class QualityAssuranceTopicsComponent implements OnInit, OnDestroy, After
}
/**
* Dispatch the Quality Assurance topics retrival.
* Dispatch the Quality Assurance topics retrieval.
*/
public getQualityAssuranceTopics(source: string, target?: string): void {
this.subs.push(this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(

View File

@@ -159,7 +159,7 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
}
/**
* Dispatch the Suggestion Targets retrival.
* Dispatch the Suggestion Targets retrieval.
*/
public getSuggestionTargets(): void {
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(

View File

@@ -31,3 +31,5 @@
<div class="ds-full-screen-loader" *ngIf="shouldShowFullscreenLoader">
<ds-loading [showMessage]="false"></ds-loading>
</div>
<ds-live-region></ds-live-region>

View File

@@ -41,6 +41,7 @@ import { ThemedFooterComponent } from '../footer/themed-footer.component';
import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component';
import { slideSidebarPadding } from '../shared/animations/slide';
import { HostWindowService } from '../shared/host-window.service';
import { LiveRegionComponent } from '../shared/live-region/live-region.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { MenuService } from '../shared/menu/menu.service';
import { MenuID } from '../shared/menu/menu-id.model';
@@ -67,6 +68,7 @@ import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banne
ThemedFooterComponent,
NotificationsBoardComponent,
AsyncPipe,
LiveRegionComponent,
],
})
export class RootComponent implements OnInit {

View File

@@ -31,7 +31,7 @@ export class SearchNavbarComponent {
// The search form
searchForm;
// Whether or not the search bar is expanded, boolean for html ngIf, string fo AngularAnimation state change
// Whether or not the search bar is expanded, boolean for html ngIf, string for AngularAnimation state change
searchExpanded = false;
isExpanded = 'collapsed';

View File

@@ -48,7 +48,7 @@ export class DsoEditMenuSectionComponent extends MenuSectionComponent implements
}
/**
* Activate the section's model funtion
* Activate the section's model function
*/
public activate(event: any) {
event.preventDefault();

View File

@@ -61,7 +61,7 @@ export class DsoWithdrawnReinstateModalService {
* @param correctionType - The type of correction.
* @param reason - The reason for the request.
* Reloads the current page in order to update the withdrawn/reinstate button.
* and desplay a notification box.
* and display a notification box.
*/
sendQARequest(target: string, correctionType: 'request-reinstate' | 'request-withdrawn', reason: string): void {
this.qaEventDataService.postData(target, correctionType, '', reason)

View File

@@ -146,7 +146,7 @@ export class EntityDropdownComponent implements OnInit, OnDestroy {
}
/**
* Method used from infitity scroll for retrive more data on scroll down
* Method used from infitity scroll for retrieve more data on scroll down
*/
public onScrollDown() {
if ( this.hasNextPage ) {

View File

@@ -43,7 +43,7 @@ describe('VocabularyTreeviewModalComponent', () => {
expect(component).toBeTruthy();
});
it('should init descrption message', () => {
it('should init description message', () => {
expect((component as any).setDescription).toHaveBeenCalled();
});
});

View File

@@ -3,6 +3,7 @@
<div class="col-12">
<div class="input-group">
<input type="text" class="form-control" [(ngModel)]="searchText" (keyup.enter)="search()"
[attr.aria-label]="'vocabulary-treeview.search.form.search-placeholder' | translate"
[placeholder]="'vocabulary-treeview.search.form.search-placeholder' | translate">
<div class="input-group-append" id="button-addon4">
<button class="btn btn-outline-primary" type="button" (click)="search()" [disabled]="!isSearchEnabled()">

View File

@@ -0,0 +1,3 @@
<div class="live-region" [ngClass]="{'visually-hidden': !isVisible }" aria-live="assertive" role="log" aria-relevant="additions" aria-atomic="true">
<div class="live-region-message" *ngFor="let message of (messages$ | async)">{{ message }}</div>
</div>

View File

@@ -0,0 +1,13 @@
.live-region {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-left: 60px;
height: 90px;
line-height: 18px;
color: var(--bs-white);
background-color: var(--bs-dark);
opacity: 0.94;
z-index: var(--ds-live-region-z-index);
}

View File

@@ -0,0 +1,62 @@
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { LiveRegionComponent } from './live-region.component';
import { LiveRegionService } from './live-region.service';
describe('liveRegionComponent', () => {
let fixture: ComponentFixture<LiveRegionComponent>;
let liveRegionService: LiveRegionService;
beforeEach(waitForAsync(() => {
liveRegionService = jasmine.createSpyObj('liveRegionService', {
getMessages$: of(['message1', 'message2']),
getLiveRegionVisibility: false,
setLiveRegionVisibility: undefined,
});
void TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
LiveRegionComponent,
],
providers: [
{ provide: LiveRegionService, useValue: liveRegionService },
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LiveRegionComponent);
fixture.detectChanges();
});
it('should contain the current live region messages', () => {
const messages = fixture.debugElement.queryAll(By.css('.live-region-message'));
expect(messages.length).toEqual(2);
expect(messages[0].nativeElement.textContent).toEqual('message1');
expect(messages[1].nativeElement.textContent).toEqual('message2');
});
it('should respect the live region visibility', () => {
const liveRegion = fixture.debugElement.query(By.css('.live-region'));
expect(liveRegion).toBeDefined();
const liveRegionHidden = fixture.debugElement.query(By.css('.visually-hidden'));
expect(liveRegionHidden).toBeDefined();
liveRegionService.getLiveRegionVisibility = jasmine.createSpy('getLiveRegionVisibility').and.returnValue(true);
fixture = TestBed.createComponent(LiveRegionComponent);
fixture.detectChanges();
const liveRegionVisible = fixture.debugElement.query(By.css('.visually-hidden'));
expect(liveRegionVisible).toBeNull();
});
});

View File

@@ -0,0 +1,42 @@
import {
AsyncPipe,
NgClass,
NgFor,
} from '@angular/common';
import {
Component,
OnInit,
} from '@angular/core';
import { Observable } from 'rxjs';
import { LiveRegionService } from './live-region.service';
/**
* The Live Region Component is an accessibility tool for screenreaders. When a change occurs on a page when the changed
* section is not in focus, a message should be displayed by this component so it can be announced by a screen reader.
*
* This component should not be used directly. Use the {@link LiveRegionService} to add messages.
*/
@Component({
selector: `ds-live-region`,
templateUrl: './live-region.component.html',
styleUrls: ['./live-region.component.scss'],
standalone: true,
imports: [NgClass, NgFor, AsyncPipe],
})
export class LiveRegionComponent implements OnInit {
protected isVisible: boolean;
protected messages$: Observable<string[]>;
constructor(
protected liveRegionService: LiveRegionService,
) {
}
ngOnInit() {
this.isVisible = this.liveRegionService.getLiveRegionVisibility();
this.messages$ = this.liveRegionService.getMessages$();
}
}

View File

@@ -0,0 +1,9 @@
import { Config } from '../../../config/config.interface';
/**
* Configuration interface used by the LiveRegionService
*/
export class LiveRegionConfig implements Config {
messageTimeOutDurationMs: number;
isVisible: boolean;
}

View File

@@ -0,0 +1,175 @@
import {
fakeAsync,
flush,
tick,
} from '@angular/core/testing';
import { UUIDService } from '../../core/shared/uuid.service';
import { LiveRegionService } from './live-region.service';
describe('liveRegionService', () => {
let service: LiveRegionService;
beforeEach(() => {
service = new LiveRegionService(
new UUIDService(),
);
});
describe('addMessage', () => {
it('should correctly add messages', () => {
expect(service.getMessages().length).toEqual(0);
service.addMessage('Message One');
expect(service.getMessages().length).toEqual(1);
expect(service.getMessages()[0]).toEqual('Message One');
service.addMessage('Message Two');
expect(service.getMessages().length).toEqual(2);
expect(service.getMessages()[1]).toEqual('Message Two');
});
});
describe('clearMessages', () => {
it('should clear the messages', () => {
expect(service.getMessages().length).toEqual(0);
service.addMessage('Message One');
service.addMessage('Message Two');
expect(service.getMessages().length).toEqual(2);
service.clear();
expect(service.getMessages().length).toEqual(0);
});
});
describe('messages$', () => {
it('should emit when a message is added and when a message is removed after the timeOut', fakeAsync(() => {
const results: string[][] = [];
service.getMessages$().subscribe((messages) => {
results.push(messages);
});
expect(results.length).toEqual(1);
expect(results[0]).toEqual([]);
service.addMessage('message');
tick();
expect(results.length).toEqual(2);
expect(results[1]).toEqual(['message']);
tick(service.getMessageTimeOutMs());
expect(results.length).toEqual(3);
expect(results[2]).toEqual([]);
}));
it('should only emit once when the messages are cleared', fakeAsync(() => {
const results: string[][] = [];
service.getMessages$().subscribe((messages) => {
results.push(messages);
});
expect(results.length).toEqual(1);
expect(results[0]).toEqual([]);
service.addMessage('Message One');
service.addMessage('Message Two');
tick();
expect(results.length).toEqual(3);
expect(results[2]).toEqual(['Message One', 'Message Two']);
service.clear();
flush();
expect(results.length).toEqual(4);
expect(results[3]).toEqual([]);
}));
it('should not pop messages added after clearing within timeOut period', fakeAsync(() => {
const results: string[][] = [];
service.getMessages$().subscribe((messages) => {
results.push(messages);
});
expect(results.length).toEqual(1);
expect(results[0]).toEqual([]);
service.addMessage('Message One');
tick(10000);
service.clear();
tick(15000);
service.addMessage('Message Two');
// Message Two should not be cleared after 5 more seconds
tick(5000);
expect(results.length).toEqual(4);
expect(results[3]).toEqual(['Message Two']);
// But should be cleared 30 seconds after it was added
tick(25000);
expect(results.length).toEqual(5);
expect(results[4]).toEqual([]);
}));
it('should respect configured timeOut', fakeAsync(() => {
const results: string[][] = [];
service.getMessages$().subscribe((messages) => {
results.push(messages);
});
expect(results.length).toEqual(1);
expect(results[0]).toEqual([]);
const timeOutMs = 500;
service.setMessageTimeOutMs(timeOutMs);
service.addMessage('Message One');
tick(timeOutMs - 1);
expect(results.length).toEqual(2);
expect(results[1]).toEqual(['Message One']);
tick(1);
expect(results.length).toEqual(3);
expect(results[2]).toEqual([]);
const timeOutMsTwo = 50000;
service.setMessageTimeOutMs(timeOutMsTwo);
service.addMessage('Message Two');
tick(timeOutMsTwo - 1);
expect(results.length).toEqual(4);
expect(results[3]).toEqual(['Message Two']);
tick(1);
expect(results.length).toEqual(5);
expect(results[4]).toEqual([]);
}));
});
describe('liveRegionVisibility', () => {
it('should be false by default', () => {
expect(service.getLiveRegionVisibility()).toBeFalse();
});
it('should correctly update', () => {
service.setLiveRegionVisibility(true);
expect(service.getLiveRegionVisibility()).toBeTrue();
service.setLiveRegionVisibility(false);
expect(service.getLiveRegionVisibility()).toBeFalse();
});
});
});

View File

@@ -0,0 +1,133 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { environment } from '../../../environments/environment';
import { UUIDService } from '../../core/shared/uuid.service';
/**
* The LiveRegionService is responsible for handling the messages that are shown by the {@link LiveRegionComponent}.
* Use this service to add or remove messages to the Live Region.
*/
@Injectable({
providedIn: 'root',
})
export class LiveRegionService {
constructor(
protected uuidService: UUIDService,
) {
}
/**
* The duration after which the messages disappear in milliseconds
* @protected
*/
protected messageTimeOutDurationMs: number = environment.liveRegion.messageTimeOutDurationMs;
/**
* Array containing the messages that should be shown in the live region,
* together with a uuid, so they can be uniquely identified
* @protected
*/
protected messages: { message: string, uuid: string }[] = [];
/**
* BehaviorSubject emitting the array with messages every time the array updates
* @protected
*/
protected messages$: BehaviorSubject<string[]> = new BehaviorSubject([]);
/**
* Whether the live region should be visible
* @protected
*/
protected liveRegionIsVisible: boolean = environment.liveRegion.isVisible;
/**
* Returns a copy of the array with the current live region messages
*/
getMessages(): string[] {
return this.messages.map(messageObj => messageObj.message);
}
/**
* Returns the BehaviorSubject emitting the array with messages every time the array updates
*/
getMessages$(): BehaviorSubject<string[]> {
return this.messages$;
}
/**
* Adds a message to the live-region messages array
* @param message
* @return The uuid of the message
*/
addMessage(message: string): string {
const uuid = this.uuidService.generate();
this.messages.push({ message, uuid });
setTimeout(() => this.clearMessageByUUID(uuid), this.messageTimeOutDurationMs);
this.emitCurrentMessages();
return uuid;
}
/**
* Clears the live-region messages array
*/
clear() {
this.messages = [];
this.emitCurrentMessages();
}
/**
* Removes the message with the given UUID from the messages array
* @param uuid The uuid of the message to clear
*/
clearMessageByUUID(uuid: string) {
const index = this.messages.findIndex(messageObj => messageObj.uuid === uuid);
if (index !== -1) {
this.messages.splice(index, 1);
this.emitCurrentMessages();
}
}
/**
* Makes the messages$ BehaviorSubject emit the current messages array
* @protected
*/
protected emitCurrentMessages() {
this.messages$.next(this.getMessages());
}
/**
* Returns a boolean specifying whether the live region should be visible.
* Returns 'true' if the region should be visible and false otherwise.
*/
getLiveRegionVisibility(): boolean {
return this.liveRegionIsVisible;
}
/**
* Sets the visibility of the live region.
* Setting this to true will make the live region visible which is useful for debugging purposes.
* @param isVisible
*/
setLiveRegionVisibility(isVisible: boolean) {
this.liveRegionIsVisible = isVisible;
}
/**
* Gets the current message timeOut duration in milliseconds
*/
getMessageTimeOutMs(): number {
return this.messageTimeOutDurationMs;
}
/**
* Sets the message timeOut duration
* @param timeOutMs the message timeOut duration in milliseconds
*/
setMessageTimeOutMs(timeOutMs: number) {
this.messageTimeOutDurationMs = timeOutMs;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Availavle Menu IDs
* Available Menu IDs
*/
export enum MenuID {
ADMIN = 'admin-sidebar',

View File

@@ -106,7 +106,7 @@ export class EndpointMockingRestService extends DspaceRestService {
* Get the mock response associated with this URL from this.mockResponseMap
*
* @param urlStr
* the URL to fetch a mock reponse for
* the URL to fetch a mock response for
* @return any
* the mock response if there is one, undefined otherwise
*/

View File

@@ -28,7 +28,7 @@ export class Duplicate implements CacheableObject {
@autoserialize
uuid: string;
/**
* The workfow item ID, if any
* The workflow item ID, if any
*/
@autoserialize
workflowItemId: number;

View File

@@ -68,7 +68,7 @@ describe('BrowseLinkMetadataListElementComponent', () => {
});
});
describe('with metadata wit an url', () => {
describe('with metadata with an url', () => {
beforeEach(() => {
comp.mdRepresentation = mockMetadataRepresentationWithUrl;
spyOnProperty(comp.mdRepresentation, 'representationType', 'get').and.returnValue(MetadataRepresentationType.BrowseLink);

View File

@@ -46,7 +46,7 @@ export class CollectionSelectComponent extends ObjectSelectComponent<Collection>
/**
* Collection of all the data that is used to display the {@link Collection} in the HTML.
* By collecting this data here it doesn't need to be recalculated on evey change detection.
* By collecting this data here it doesn't need to be recalculated on every change detection.
*/
selectCollections$: Observable<DSpaceObjectSelect<Collection>[]>;

View File

@@ -50,7 +50,7 @@ export class ItemSelectComponent extends ObjectSelectComponent<Item> implements
/**
* Collection of all the data that is used to display the {@link Item} in the HTML.
* By collecting this data here it doesn't need to be recalculated on evey change detection.
* By collecting this data here it doesn't need to be recalculated on every change detection.
*/
selectItems$: Observable<DSpaceObjectSelect<Item>[]>;

View File

@@ -33,7 +33,7 @@ import { PaginatedSearchOptions } from '../search/models/paginated-search-option
/**
* The Rss feed button componenet.
* The Rss feed button component.
*/
@Component({
exportAs: 'rssComponent',

View File

@@ -73,7 +73,7 @@ export class SearchOptions {
get encodedFixedFilter(): string {
// expected format: 'arg=value'
// -> split the query agument into (arg=)(value) and only encode 'value'
// -> split the query argument into (arg=)(value) and only encode 'value'
const match = this.fixedFilter.match(/^([^=]+=)(.+)$/);
if (hasValue(match)) {

View File

@@ -118,7 +118,7 @@ describe('SearchFacetRangeOptionComponent', () => {
fixture.detectChanges();
});
describe('when the updateChangeParams method is called wih a value', () => {
describe('when the updateChangeParams method is called with a value', () => {
it('should update the changeQueryParams with the new parameter values', () => {
comp.changeQueryParams = {};
comp.filterValue = {

View File

@@ -6,7 +6,7 @@
[attr.aria-label]="(((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate) + ' ' + (('search.filters.filter.' + filter.name + '.head') | translate | lowercase)"
[attr.data-test]="'filter-toggle' | dsBrowserOnly"
>
<span class="h4 d-inline-block text-left mt-auto mb-auto">
<span class="h4 d-inline-block text-left mt-auto mb-auto dark:text-white text-dark">
{{'search.filters.filter.' + filter.name + '.head'| translate}}
</span>
<i class="filter-toggle flex-grow-1 fas p-auto"

View File

@@ -98,7 +98,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
/**
* Whether the sider is being controlled by the keyboard.
* Supresses any changes until the key is released.
* Suppresses any changes until the key is released.
*/
keyboardControl: boolean;

View File

@@ -115,7 +115,7 @@ describe('SubscriptionViewComponent', () => {
expect(de.query(By.css('.subscription-type'))).toBeTruthy();
});
it('should have subscription paramenter info', () => {
it('should have subscription parameter info', () => {
expect(de.query(By.css('.subscription-parameters > span'))).toBeTruthy();
});

View File

@@ -8,21 +8,20 @@ import {
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { environment } from '../../../environments/environment.test';
import { MathService } from '../../core/shared/math.service';
import { MockMathService } from '../../core/shared/math.service.spec';
import { MarkdownDirective } from './markdown.directive';
@Component({
template: `<div dsMarkdown="test"></div>`,
template: `<div [dsMarkdown]="'test<script>alert(1);</script>'"></div>`,
standalone: true,
imports: [ MarkdownDirective ],
})
class TestComponent {}
describe('MarkdownDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let divEl: DebugElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -32,12 +31,61 @@ describe('MarkdownDirective', () => {
}).compileComponents();
spyOn(MarkdownDirective.prototype, 'render');
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
divEl = fixture.debugElement.query(By.css('div'));
});
it('should call render method', () => {
fixture.detectChanges();
expect(MarkdownDirective.prototype.render).toHaveBeenCalled();
});
});
describe('MarkdownDirective sanitization with markdown disabled', () => {
let fixture: ComponentFixture<TestComponent>;
let divEl: DebugElement;
// Disable markdown
environment.markdown.enabled = false;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: MathService, useClass: MockMathService },
],
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
divEl = fixture.debugElement.query(By.css('div'));
});
it('should sanitize the script element out of innerHTML (markdown disabled)',() => {
fixture.detectChanges();
divEl = fixture.debugElement.query(By.css('div'));
expect(divEl.nativeElement.innerHTML).toEqual('test');
});
});
describe('MarkdownDirective sanitization with markdown enabled', () => {
let fixture: ComponentFixture<TestComponent>;
let divEl: DebugElement;
// Enable markdown
environment.markdown.enabled = true;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: MathService, useClass: MockMathService },
],
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
divEl = fixture.debugElement.query(By.css('div'));
});
it('should sanitize the script element out of innerHTML (markdown enabled)',() => {
fixture.detectChanges();
divEl = fixture.debugElement.query(By.css('div'));
expect(divEl.nativeElement.innerHTML).toEqual('test');
});
});

View File

@@ -55,7 +55,7 @@ export class MarkdownDirective implements OnInit, OnDestroy {
async render(value: string, forcePreview = false): Promise<SafeHtml> {
if (isEmpty(value) || (!environment.markdown.enabled && !forcePreview)) {
this.el.innerHTML = value;
this.el.innerHTML = this.sanitizer.sanitize(SecurityContext.HTML, value);
return;
} else {
if (environment.markdown.mathjax) {

View File

@@ -135,7 +135,7 @@ export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent
/**
* Method called when section is initialized
* Retriev available NotifyConfigs
* Retrieve available NotifyConfigs
*/
setCoarNotifyConfig() {
this.subs.push(

View File

@@ -124,7 +124,7 @@ export class SectionsService {
});
});
// Itereate over the previous error list
// Iterate over the previous error list
prevErrors.forEach((error: SubmissionSectionError) => {
const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);

View File

@@ -11,7 +11,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { Metadata } from '../../../../core/submission/models/sherpa-policies-details.model';
/**
* This component represents a section that contains the matadata informations.
* This component represents a section that contains the metadata information.
*/
@Component({
selector: 'ds-metadata-information',

View File

@@ -14,7 +14,7 @@ import { AlertType } from '../../../../shared/alert/alert-type';
import { ContentAccordionComponent } from '../content-accordion/content-accordion.component';
/**
* This component represents a section that contains the publisher policy informations.
* This component represents a section that contains the publisher policy information.
*/
@Component({
selector: 'ds-publisher-policy',

View File

@@ -35,7 +35,7 @@ import { PublicationInformationComponent } from './publication-information/publi
import { PublisherPolicyComponent } from './publisher-policy/publisher-policy.component';
/**
* This component represents a section for the sherpa policy informations structure.
* This component represents a section for the sherpa policy information structure.
*/
@Component({
selector: 'ds-section-sherpa-policies',

View File

@@ -170,7 +170,7 @@ describe('SystemWideAlertFormComponent', () => {
});
describe('save', () => {
it('should update the exising alert with the form values and show a success notification on success and navigate back', () => {
it('should update the existing alert with the form values and show a success notification on success and navigate back', () => {
spyOn(comp, 'back');
comp.currentAlert = systemWideAlert;
@@ -193,7 +193,7 @@ describe('SystemWideAlertFormComponent', () => {
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts');
expect(comp.back).toHaveBeenCalled();
});
it('should update the exising alert with the form values and show a success notification on success and not navigate back when false is provided to the save method', () => {
it('should update the existing alert with the form values and show a success notification on success and not navigate back when false is provided to the save method', () => {
spyOn(comp, 'back');
comp.currentAlert = systemWideAlert;
@@ -216,7 +216,7 @@ describe('SystemWideAlertFormComponent', () => {
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts');
expect(comp.back).not.toHaveBeenCalled();
});
it('should update the exising alert with the form values but add an empty countdown date when disabled and show a success notification on success', () => {
it('should update the existing alert with the form values but add an empty countdown date when disabled and show a success notification on success', () => {
spyOn(comp, 'back');
comp.currentAlert = systemWideAlert;
@@ -239,7 +239,7 @@ describe('SystemWideAlertFormComponent', () => {
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts');
expect(comp.back).toHaveBeenCalled();
});
it('should update the exising alert with the form values and show a error notification on error', () => {
it('should update the existing alert with the form values and show a error notification on error', () => {
spyOn(comp, 'back');
(systemWideAlertDataService.put as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
comp.currentAlert = systemWideAlert;

View File

@@ -9,8 +9,6 @@
// "401.unauthorized": "unauthorized",
"401.unauthorized": "unautorisiert",
// "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.",
"403.help": "Sie sind nicht berechtigt, auf diese Seite zuzugreifen. Über den Button unten auf der Seite gelangen Sie zurück zur Startseite.",
@@ -20,8 +18,6 @@
// "403.forbidden": "forbidden",
"403.forbidden": "verboten",
// "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
"404.help": "Die Seite konnte nicht gefunden werden. Eventuell wurde sie verschoben oder gelöscht. Über den Button unten auf der Seite gelangen Sie zurück zur Startseite.",
@@ -31,6 +27,16 @@
// "404.page-not-found": "page not found",
"404.page-not-found": "Seite nicht gefunden",
// "500.page-internal-server-error": "Service unavailable",
"500.page-internal-server-error": "Dienst nicht verfügbar",
// "500.help": "The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.",
"500.help": "Der Dienst steht momentan nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
// "500.link.home-page": "Take me to the home page",
"500.link.home-page": "Zur Startseite",
// "admin.access-control.epeople.breadcrumbs": "EPeople",
"admin.access-control.epeople.breadcrumbs": "Personen suchen",
@@ -3450,7 +3456,7 @@
"journalissue.listelement.badge": "Zeitschriftenheft",
// "journalissue.page.description": "Description",
"journalissue.page.description": "Beschreibeung",
"journalissue.page.description": "Beschreibung",
// "journalissue.page.edit": "Edit this item",
"journalissue.page.edit": "Dieses Item bearbeiten",
@@ -5560,10 +5566,10 @@
"submission.sections.general.deposit_error_notice": "Beim Einreichen des Items ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.",
// "submission.sections.general.deposit_success_notice": "Submission deposited successfully.",
"submission.sections.general.deposit_success_notice": "Veröffentlichung erfolgreich eingereicht",
"submission.sections.general.deposit_success_notice": "Veröffentlichung erfolgreich eingereicht.",
// "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.",
"submission.sections.general.discard_error_notice": "Beim Verwerfen der Einreichung ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal",
"submission.sections.general.discard_error_notice": "Beim Verwerfen der Einreichung ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.",
// "submission.sections.general.discard_success_notice": "Submission discarded successfully.",
"submission.sections.general.discard_success_notice": "Einreichung erfolgreich verworfen.",

View File

@@ -1460,7 +1460,7 @@
"community.edit.notifications.unauthorized": "You do not have privileges to make this change",
"community.edit.notifications.error": "An error occured while editing the community",
"community.edit.notifications.error": "An error occurred while editing the community",
"community.edit.return": "Back",
@@ -1670,7 +1670,7 @@
"curation.form.submit.error.head": "Running the curation task failed",
"curation.form.submit.error.content": "An error occured when trying to start the curation task.",
"curation.form.submit.error.content": "An error occurred when trying to start the curation task.",
"curation.form.submit.error.invalid-handle": "Couldn't determine the handle for this object",
@@ -1864,6 +1864,8 @@
"error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.",
"error.validation.cclicense.required": "You must grant this cclicense to complete your submission. If you are unable to grant the cclicense at this time, you may save your work and return later or remove the submission.",
"error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.",
"error.validation.filerequired": "The file upload is mandatory",
@@ -1932,7 +1934,7 @@
"forgot-email.form.error.head": "Error when trying to reset password",
"forgot-email.form.error.content": "An error occured when attempting to reset the password for the account associated with the following email address: {{ email }}",
"forgot-email.form.error.content": "An error occurred when attempting to reset the password for the account associated with the following email address: {{ email }}",
"forgot-password.title": "Forgot Password",
@@ -4108,7 +4110,7 @@
"register-page.registration.error.head": "Error when trying to register email",
"register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}",
"register-page.registration.error.content": "An error occurred when registering the following email address: {{ email }}",
"register-page.registration.error.recaptcha": "Error when trying to authenticate with recaptcha",

View File

@@ -6,6 +6,7 @@ import {
import { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
import { HALDataService } from '../app/core/data/base/hal-data-service.interface';
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
import { ActuatorsConfig } from './actuators.config';
import { AuthConfig } from './auth-config.interfaces';
import { BrowseByConfig } from './browse-by-config.interface';
@@ -33,6 +34,7 @@ import { SuggestionConfig } from './suggestion-config.interfaces';
import { ThemeConfig } from './theme.config';
import { UIServerConfig } from './ui-server-config.interface';
interface AppConfig extends Config {
ui: UIServerConfig;
rest: ServerConfig;
@@ -63,6 +65,7 @@ interface AppConfig extends Config {
qualityAssuranceConfig: QualityAssuranceConfig;
search: SearchConfig;
notifyMetrics: AdminNotifyMetricsRow[];
liveRegion: LiveRegionConfig;
}
/**

View File

@@ -1,5 +1,6 @@
import { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
import { RestRequestMethod } from '../app/core/data/rest-request-method';
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
import { ActuatorsConfig } from './actuators.config';
import { AppConfig } from './app-config.interface';
@@ -591,4 +592,10 @@ export class DefaultAppConfig implements AppConfig {
],
},
];
// Live Region configuration, used by the LiveRegionService
liveRegion: LiveRegionConfig = {
messageTimeOutDurationMs: 30000,
isVisible: false,
};
}

View File

@@ -422,4 +422,9 @@ export const environment: BuildConfig = {
],
},
],
liveRegion: {
messageTimeOutDurationMs: 30000,
isVisible: false,
},
};

View File

@@ -32,9 +32,11 @@ import { AppState } from '../../app/app.reducer';
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
import { AuthService } from '../../app/core/auth/auth.service';
import { coreSelector } from '../../app/core/core.selectors';
import { RequestService } from '../../app/core/data/request.service';
import { RootDataService } from '../../app/core/data/root-data.service';
import { LocaleService } from '../../app/core/locale/locale.service';
import { HeadTagService } from '../../app/core/metadata/head-tag.service';
import { HALEndpointService } from '../../app/core/shared/hal-endpoint.service';
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
import { InitService } from '../../app/init.service';
import { KlaroService } from '../../app/shared/cookies/klaro.service';
@@ -81,6 +83,9 @@ export class BrowserInitService extends InitService {
protected menuService: MenuService,
private rootDataService: RootDataService,
protected router: Router,
private requestService: RequestService,
private halService: HALEndpointService,
) {
super(
store,
@@ -169,17 +174,15 @@ export class BrowserInitService extends InitService {
}
/**
* During an external authentication flow invalidate the SSR transferState
* During an external authentication flow invalidate the
* data in the cache. This allows the app to fetch fresh content.
* @private
*/
private externalAuthCheck() {
this.sub = this.authService.isExternalAuthentication().pipe(
filter((externalAuth: boolean) => externalAuth),
).subscribe(() => {
// Clear the transferState data.
this.rootDataService.invalidateRootCache();
this.requestService.setStaleByHrefSubstring(this.halService.getRootHref());
this.authService.setExternalAuthStatus(false);
},
);

View File

@@ -85,7 +85,7 @@ export class ServerInitService extends InitService {
// Server-only initialization steps
/**
* Set the {@link NGRX_STATE} key when state is serialized to be transfered
* Set the {@link NGRX_STATE} key when state is serialized to be transferred
* @private
*/
private saveAppState() {

View File

@@ -13,6 +13,7 @@
--ds-login-logo-width:72px;
--ds-submission-header-z-index: 1001;
--ds-submission-footer-z-index: 999;
--ds-live-region-z-index: 1030;
--ds-main-z-index: 1;
--ds-nav-z-index: 10;

View File

@@ -13,6 +13,7 @@ import { ThemedFooterComponent } from '../../../../app/footer/themed-footer.comp
import { ThemedHeaderNavbarWrapperComponent } from '../../../../app/header-nav-wrapper/themed-header-navbar-wrapper.component';
import { RootComponent as BaseComponent } from '../../../../app/root/root.component';
import { slideSidebarPadding } from '../../../../app/shared/animations/slide';
import { LiveRegionComponent } from '../../../../app/shared/live-region/live-region.component';
import { ThemedLoadingComponent } from '../../../../app/shared/loading/themed-loading.component';
import { NotificationsBoardComponent } from '../../../../app/shared/notifications/notifications-board/notifications-board.component';
import { SystemWideAlertBannerComponent } from '../../../../app/system-wide-alert/alert-banner/system-wide-alert-banner.component';
@@ -38,6 +39,7 @@ import { SystemWideAlertBannerComponent } from '../../../../app/system-wide-aler
ThemedFooterComponent,
NotificationsBoardComponent,
AsyncPipe,
LiveRegionComponent,
],
})
export class RootComponent extends BaseComponent {

2
src/typings.d.ts vendored
View File

@@ -9,7 +9,7 @@ declare module "my-module" {
export function doesSomething(value: string): string;
}
*
* If you're prototying and you will fix the types later you can also declare it as type any
* If you're prototyping and you will fix the types later you can also declare it as type any
*
declare var assert: any;
*