Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
75992e3
Fix incorrect translation of "withdraw" (#1191)
Paurikova2 Feb 10, 2026
f9ddc44
UFAL/Removed green line from the home page (#1204)
milanmajchrak Feb 10, 2026
c794f52
UFAL/Added import-5 to deploy (#1206)
Paurikova2 Feb 12, 2026
1b769b9
UFAL/Removed unused github docker registry (#1212)
milanmajchrak Feb 18, 2026
d4d84cb
internal/Renamed dspace-import to dspace-import-clarin in import acti…
Paurikova2 Feb 18, 2026
0ee58b2
UFAL/Editing similar process parameters (#1195)
Paurikova2 Feb 18, 2026
2f14694
UFAL/Display all versions in version history (#1213)
Kasinhou Feb 19, 2026
aac563e
UFAL/Missing translation for new curation task (#1208)
kosarko Feb 19, 2026
0434ff9
UFAL/We expect (in metadata) the path to be "licence" (#1193)
kosarko Feb 19, 2026
99637f0
UFAL/Rewrite OAI links in static page HTML with rest.baseUrl (ufal/ds…
kosarko Feb 19, 2026
d6ed972
Update path for dspace-import in action.yml (#1216)
Kasinhou Feb 23, 2026
6fc73d8
Refactor erase-db action to clean up comments
vidiecan Feb 24, 2026
911bb36
Use `dspace:dspace` permissions for the assetstore - not ubuntu:ubunt…
milanmajchrak Feb 24, 2026
1dd1127
UFAL/Fix: generate correct curl download URLs using backend handle en…
milanmajchrak Feb 26, 2026
6ea61f9
UFAL/CURL downloads issue - fix integration test
milanmajchrak Feb 26, 2026
31adb31
UFAL/Fix: preserve namespace in OAI link rewrite on static pages (#12…
milanmajchrak Feb 27, 2026
6628aaf
UFAL/Resolve duplicate HTML element IDs across pages (#1221)
Kasinhou Mar 2, 2026
4bc3119
Refactor running rest tests in deploy
Mar 4, 2026
d175c4a
Commented playwright for testing
Mar 4, 2026
6e743aa
UFAL/Successfully downloaded collection file when the file is downloa…
Paurikova2 Mar 4, 2026
dcce53c
Removed dynamic string interpolation from URL
Mar 4, 2026
24717a5
Allows running playwright tests again
Mar 9, 2026
eed7d30
UFAL/Resolve duplicate html IDs in submission form (#1234)
Kasinhou Mar 9, 2026
b920059
Merge pull request #1233 from dataquest-dev/ufal/update-rest-tests-ca…
Kasinhou Mar 9, 2026
39abed3
Revert "UFAL/Refactor running rest tests in deploy" (#1238)
Kasinhou Mar 9, 2026
602a261
UFAL/Broken matomo tracking with custom dimensions (#1237)
milanmajchrak Mar 10, 2026
97658b0
Merge branch 'dtq-dev' into ufal/release-hotfix-2026-03-11
milanmajchrak Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ <h2 id="search" class="border-bottom pb-2">
</div>
<div class="flex-grow-1 mr-3 ml-3">
<div class="form-group input-group">
<label id="query" for="query" class="sr-only">{{labelPrefix + 'search.placeholder' | translate}}</label>
<label id="query-label" for="query" class="sr-only">{{labelPrefix + 'search.placeholder' | translate}}</label>
<input type="text" name="query" id="query" formControlName="query"
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
Expand Down
3 changes: 2 additions & 1 deletion src/app/bitstream-page/bitstream-page.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
* Requesting them as embeds will limit the number of requests
*/
export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Bitstream>[] = [
followLink('bundle', {}, followLink('primaryBitstream'), followLink('item')),
followLink('bundle', {}, followLink('item')),
followLink('bundle', {}, followLink('primaryBitstream')),
followLink('format')
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</div>

<div class="d-flex align-items-start clarin-ref-box-footer-services pt-1">
<div class="btn-group pr-2" ngbDropdown *ngFor="let featuredService of (featuredServices | async)">
<div class="btn-group pr-2" ngbDropdown *ngFor="let featuredService of (featuredServices | async); let i = index">
<a class="btn btn-danger clarin-ref-box-services-button"
type="button"
target="_blank"
Expand All @@ -22,13 +22,13 @@
</a>
<button type="button"
class="btn btn-danger dropdown-toggle dropdown-toggle-split clarin-ref-box-services-button"
id="resultdropdown"
id="resultdropdown-{{ i }}"
ngbDropdownToggle
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
</button>
<div ngbDropdownMenu aria-labelledby="resultdropdown" class="lindat-dropdown-menu">
<div ngbDropdownMenu [attr.aria-labelledby]="'resultdropdown-' + i" class="lindat-dropdown-menu">
<a
class="dropdown-item"
target="_blank"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@
[attr.aria-label]="'item.edit.bitstreams.bundle.table.aria-label' | translate: { bundle: bundleName } ">
<thead [class.visually-hidden]="!isFirstTable">
<tr class="header-row font-weight-bold">
<th id="name" scope="col" class="{{ columnSizes.columns[0].buildClasses() }}">
<th id="name-{{ sanitizedBundleName }}" scope="col" class="{{ columnSizes.columns[0].buildClasses() }}">
{{'item.edit.bitstreams.headers.name' | translate}}
</th>
<th id="description" scope="col" class="{{ columnSizes.columns[1].buildClasses() }}">
<th id="description-{{ sanitizedBundleName }}" scope="col" class="{{ columnSizes.columns[1].buildClasses() }}">
{{'item.edit.bitstreams.headers.description' | translate}}
</th>
<th id="format" scope="col" class="{{ columnSizes.columns[2].buildClasses() }}">
<th id="format-{{ sanitizedBundleName }}" scope="col" class="{{ columnSizes.columns[2].buildClasses() }}">
{{'item.edit.bitstreams.headers.format' | translate}}
</th>
<th id="actions" scope="col" class="{{ columnSizes.columns[3].buildClasses() }} text-center">
<th id="actions-{{ sanitizedBundleName }}" scope="col" class="{{ columnSizes.columns[3].buildClasses() }} text-center">
{{'item.edit.bitstreams.headers.actions' | translate}}
</th>
<th id="synchronized" scope="col" class="{{ columnSizes.columns[4].buildClasses() }} text-center">
<th id="synchronized-{{ sanitizedBundleName }}" scope="col" class="{{ columnSizes.columns[4].buildClasses() }} text-center">
{{'item.edit.bitstreams.headers.synchronized' | translate}}
</th>
</tr>
Expand All @@ -50,19 +50,19 @@
</button>
<div ngbDropdown #paginationControls="ngbDropdown" class="btn-group float-right btn-sm p-0"
placement="bottom-right">
<button class="btn btn-outline-secondary" id="paginationControls" ngbDropdownToggle
<button class="btn btn-outline-secondary" id="paginationControls-bundle-{{ sanitizedBundleName }}" 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"
<ul id="paginationControlsDropdownMenu-bundle-{{ sanitizedBundleName }}" [attr.aria-labelledby]="'paginationControls-bundle-' + sanitizedBundleName" role="menu"
ngbDropdownMenu>
<li role="menuitem">
<span class="dropdown-header" id="pagination-control_results-per-page"
<span class="dropdown-header" id="pagination-control_results-per-page-bundle-{{ sanitizedBundleName }}"
role="heading">{{ 'pagination.results-per-page' | translate}}</span>
<ul aria-labelledby="pagination-control_results-per-page" class="list-unstyled" role="listbox">
<ul [attr.aria-labelledby]="'pagination-control_results-per-page-bundle-' + sanitizedBundleName" class="list-unstyled" role="listbox">
<li *ngFor="let size of paginationOptions.pageSizeOptions" role="option"
[attr.aria-selected]="size === (pageSize$ | async)">
<button (click)="doPageSizeChange(size)" class="dropdown-item">
Expand All @@ -85,7 +85,7 @@
(cdkDragStarted)="dragStart()" (cdkDragEnded)="dragEnd()">

<th class="bitstream-name row-element {{ columnSizes.columns[0].buildClasses() }}"
scope="row" id="{{ entry.nameStripped }}" headers="{{ bundleName }} name">
scope="row" id="{{ entry.nameStripped }}" headers="{{ bundleName }} name-{{ sanitizedBundleName }}">
<div class="drag-handle text-muted float-left p-1 mr-2" tabindex="0" cdkDragHandle
(keydown.enter)="select($event, entry)" (keydown.space)="select($event, entry)" (click)="select($event, entry)">
Comment on lines 87 to 90
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headers attributes still include bundleName as an ID token (e.g. headers="{{ bundleName }} name-..."). Since bundleName may contain whitespace (as noted in the new TS comment), this can produce invalid header references for screen readers. Use a consistently-sanitized ID value for both the referenced header cell id and all headers tokens (and keep bundleName only for visible text).

Copilot uses AI. Check for mistakes.
<i class="fas fa-grip-vertical fa-fw"
Expand All @@ -94,15 +94,15 @@
<span class="dont-break-out">{{ entry.name }}</span>
</th>
<td class="row-element {{ columnSizes.columns[1].buildClasses() }}"
headers="{{ entry.nameStripped }} {{ bundleName }} description">
headers="{{ entry.nameStripped }} {{ bundleName }} description-{{ sanitizedBundleName }}">
<span class="dont-break-out">{{ entry.description }}</span>
</td>
<td class="row-element {{ columnSizes.columns[2].buildClasses() }}"
headers="{{ entry.nameStripped }} {{ bundleName }} format">
headers="{{ entry.nameStripped }} {{ bundleName }} format-{{ sanitizedBundleName }}">
<span class="dont-break-out">{{ (entry.format | async)?.shortDescription }}</span>
</td>
<td class="row-element {{ columnSizes.columns[3].buildClasses() }}"
headers="{{ entry.nameStripped }} {{ bundleName }} actions">
headers="{{ entry.nameStripped }} {{ bundleName }} actions-{{ sanitizedBundleName }}">
<div class="text-center w-100">
<div class="btn-group relationship-action-buttons">
<a [href]="entry.downloadUrl"
Expand Down Expand Up @@ -133,7 +133,7 @@
</div>
</td>
<td class="{{columnSizes.columns[4].buildClasses()}} row-element"
headers="{{ entry.nameStripped }} {{ bundleName }} synchronized">
headers="{{ entry.nameStripped }} {{ bundleName }} synchronized-{{ sanitizedBundleName }}">
<div class="d-flex align-items-center justify-content-center">
<span class="text-center">
<i [class]="isBitstreamSynchronized(entry) ? 'fas fa-check' : 'fas fa-times'"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy {
*/
bundleName: string;

/**
* Sanitized bundle name (whitespace removed) for use in HTML element IDs.
* HTML IDs cannot contain spaces, so this ensures valid id/headers/aria-labelledby tokens.
*/
sanitizedBundleName: string;

/**
* The number of bitstreams in the bundle
*/
Expand Down Expand Up @@ -189,6 +195,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy {
this.viewContainerRef.createEmbeddedView(this.bundleView);
this.itemPageRoute = getItemPageRoute(this.item);
this.bundleName = this.dsoNameService.getName(this.bundle);
this.sanitizedBundleName = this.bundleName.replace(/\s+/g, '');
this.bundleUrl = this.bundle.self;

this.initializePagination();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<div *ngVar="item?.allMetadataValues('local.sponsor') as sponsorMvs">
<div *ngFor="let sponsor of sponsorMvs; let lastItem = last">
<div *ngFor="let sponsor of sponsorMvs; let i = index; let lastItem = last">
<div *ngVar="sponsor?.split(SPONSOR_VALUE_SEPARATOR) as sponsorMetadataValueParts">
<div class="sponsor-padding">
<span id="organization-value">{{ getValueOrError(sponsorMetadataValueParts?.[2], ORGANIZATION_ERROR) }}</span>
<span id="organization-value-{{ i }}">{{ getValueOrError(sponsorMetadataValueParts?.[2], ORGANIZATION_ERROR) }}</span>
</div>
<div class="sponsor-padding">
<span>{{ 'item.page.acknowledgement.project-code' | translate }}</span>
<span id="project-code-value" class="pl-1">{{ getValueOrError(sponsorMetadataValueParts?.[1], PROJECT_CODE_ERROR) }}</span>
<span id="project-code-value-{{ i }}" class="pl-1">{{ getValueOrError(sponsorMetadataValueParts?.[1], PROJECT_CODE_ERROR) }}</span>
</div>
<div class="sponsor-padding">
<span>{{ 'item.page.acknowledgement.project-name' | translate }}</span>
<span id="project-name-value" class="pl-1">{{ getValueOrError(sponsorMetadataValueParts?.[3], PROJECT_NAME_ERROR) }}</span>
<span id="project-name-value-{{ i }}" class="pl-1">{{ getValueOrError(sponsorMetadataValueParts?.[3], PROJECT_NAME_ERROR) }}</span>
</div>
<div *ngIf="!lastItem" class="pb-3"></div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ describe('ClarinSponsorItemFieldComponent', () => {
component.item = mockItemWithMetadataFieldsAndValue(['local.sponsor'], EU_FUND);
fixture.detectChanges();

expect(fixture.debugElement.query(By.css('#organization-value')).nativeElement.textContent).toContain(ORGANIZATION);
expect(fixture.debugElement.query(By.css('#project-code-value')).nativeElement.textContent).toContain(PROJECT_CODE);
expect(fixture.debugElement.query(By.css('#project-name-value')).nativeElement.textContent).toContain(PROJECT_NAME);
expect(fixture.debugElement.query(By.css('#organization-value-0')).nativeElement.textContent).toContain(ORGANIZATION);
expect(fixture.debugElement.query(By.css('#project-code-value-0')).nativeElement.textContent).toContain(PROJECT_CODE);
expect(fixture.debugElement.query(By.css('#project-name-value-0')).nativeElement.textContent).toContain(PROJECT_NAME);
});

it('should render national fund', () => {
Expand All @@ -54,8 +54,8 @@ describe('ClarinSponsorItemFieldComponent', () => {

component.item = mockItemWithMetadataFieldsAndValue(['local.sponsor'], NATIONAL_FUND) as any;
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('#organization-value')).nativeElement.textContent).toContain(ORGANIZATION);
expect(fixture.debugElement.query(By.css('#project-code-value')).nativeElement.textContent).toContain(PROJECT_CODE);
expect(fixture.debugElement.query(By.css('#project-name-value')).nativeElement.textContent).toContain(PROJECT_NAME);
expect(fixture.debugElement.query(By.css('#organization-value-0')).nativeElement.textContent).toContain(ORGANIZATION);
expect(fixture.debugElement.query(By.css('#project-code-value-0')).nativeElement.textContent).toContain(PROJECT_CODE);
expect(fixture.debugElement.query(By.css('#project-name-value-0')).nativeElement.textContent).toContain(PROJECT_NAME);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<ng-template #fileTemplate>
<i class="far fa-file fa-sm pr-1" style="color: #a0a0a0"></i>
<span id="folderName">{{ node.name }}</span>
<span>{{ node.name }}</span>
<span class="size pull-right">{{ node.size }}</span>
</ng-template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('FileTreeViewComponent', () => {

it('should display the node name', () => {
waitForAsync(() => {
const nodeNameElement = fixture.debugElement.query(By.css('#folderName')).nativeElement;
const nodeNameElement = fixture.debugElement.query(By.css('span')).nativeElement;
expect(nodeNameElement.textContent).toContain('TestFolder');
});
Comment on lines 47 to 51
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In should display the node name, waitForAsync is being called but its returned wrapper function is never used by Jasmine, so the assertions inside don’t actually run. Convert this to it('...', waitForAsync(() => { ... })) (or remove waitForAsync and keep the assertions synchronous) so the test truly validates the DOM content.

Copilot uses AI. Check for mistakes.
});
Expand Down
10 changes: 5 additions & 5 deletions src/app/shared/ds-select/ds-select.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
<div ngbDropdown class="btn-group" (openChange)="toggled.emit($event)">

<div class="input-group-prepend" *ngIf="label">
<span id="dsSelectMenuLabel" class="input-group-text">
<span id="dsSelectMenuLabel-{{ uniqueId }}" class="input-group-text">
{{ label | translate }}
</span>
</div>

<button aria-describedby="dsSelectMenuLabel"
id="dsSelectMenuButton"
<button [attr.aria-describedby]="label ? 'dsSelectMenuLabel-' + uniqueId : null"
id="dsSelectMenuButton-{{ uniqueId }}"
class="btn btn-outline-primary selection"
(blur)="close.emit($event)"
(click)="close.emit($event)"
Expand All @@ -20,8 +20,8 @@

<div ngbDropdownMenu
class="dropdown-menu"
id="dsSelectDropdownMenu"
aria-labelledby="dsSelectMenuButton">
id="dsSelectDropdownMenu-{{ uniqueId }}"
[attr.aria-labelledby]="'dsSelectMenuButton-' + uniqueId">
<div>
<ng-content select=".menu"></ng-content>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/app/shared/ds-select/ds-select.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';

let nextDsSelectId = 0;

/**
* Component which represent a DSpace dropdown selector.
*/
Expand All @@ -10,6 +12,11 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
})
export class DsSelectComponent {

/**
* Unique identifier for the component instance.
*/
uniqueId = `ds-select-${nextDsSelectId++}`;

/**
* An optional label for the dropdown selector.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
<label *ngIf="!isCheckbox && hasLabel"
[id]="'label_' + model.id"
[id]="'label_' + id"
[for]="id"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,78 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent);
});

describe('unique id generation', () => {
afterEach(() => {
DsDynamicFormControlContainerComponent.resetIdCounters();
});

it('should return the base element ID for the first instance of a given id', () => {
// testModel (formModel[8]) has id 'input' — first instance keeps original
expect(component.id).toBe(component.model.id);
});

it('should return a suffixed ID for the second instance of the same base id', () => {
// Simulate two separate container instances with the same base id.
// The first call to the getter registers 'input' → suffix 0 (original).
expect(component.id).toBe('input');

// Create a second component-like access: directly exercise the getter
// on a fresh component that shares the same model id.
const secondComponent = Object.create(component);
secondComponent._cachedId = undefined;
secondComponent._baseId = undefined;
// model with the same id but a new instance
secondComponent.model = new DynamicInputModel({ id: 'input' });
expect(secondComponent.id).toBe('input_1');
});

it('should not interfere between different base ids', () => {
expect(component.id).toBe('input'); // registers 'input'

const otherComponent = Object.create(component);
otherComponent._cachedId = undefined;
otherComponent._baseId = undefined;
otherComponent.model = new DynamicInputModel({ id: 'email' });
expect(otherComponent.id).toBe('email'); // first 'email' → original
});

it('should return the same id on repeated access (idempotent)', () => {
const first = component.id;
const second = component.id;
expect(first).toBe(second);
});

it('should remove the id state entry so the next instance reuses the base id', () => {
expect(component.id).toBe('input');

// Destroy the only active instance — state entry should be deleted
component.ngOnDestroy();
// A new component with the same base id should receive the original base id (no suffix)
const newComponent = Object.create(component);
newComponent._cachedId = undefined;
newComponent._baseId = undefined;
newComponent.model = new DynamicInputModel({ id: 'input' });
expect(newComponent.id).toBe('input');
});

it('should keep the id state entry when other instances with the same base id are still active', () => {
// Register two instances with the same base id
expect(component.id).toBe('input');

const secondComponent = Object.create(component);
secondComponent._cachedId = undefined;
secondComponent._baseId = undefined;
secondComponent.model = new DynamicInputModel({ id: 'input' });
expect(secondComponent.id).toBe('input_1');

component.ngOnDestroy();// destroy only the first instance
// A third component with the same base id should still get a suffixed id
const thirdComponent = Object.create(component);
thirdComponent._cachedId = undefined;
thirdComponent._baseId = undefined;
thirdComponent.model = new DynamicInputModel({ id: 'input' });
expect(thirdComponent.id).toBe('input_2');
});
});

});
Loading
Loading