From c794f52b72c8cb06343667729e3a92966f18ac8f Mon Sep 17 00:00:00 2001 From: Paurikova2 <107862249+Paurikova2@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:16:36 +0100 Subject: [PATCH 01/23] UFAL/Added import-5 to deploy (#1206) --- .github/workflows/deploy.yml | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e6104dc8775..951a56d570c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -109,6 +109,48 @@ jobs: /bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://dev-5.pc:8$INSTANCE/repository/server/api)" != "200" ]]; do sleep 5; done' + import-5: + runs-on: dspace-dep-1 + if: inputs.IMPORT + needs: deploy-5 + env: + INSTANCE: '5' + ENVFILE: /opt/dspace-envs/.env.dspace.dev-5 + steps: + - uses: ./.github/actions/import-db + with: + INSTANCE: ${{ env.INSTANCE }} + DATADIR: /opt/dspace-data/clarin-dspace-oxford/ + ASSETSTORE: /opt/dspace-data/clarin-dspace-oxford/assetstore/ + LOGDIR: /log/ + ADMIN_PASSWORD: ${{ secrets.DSPACE_ADMIN_PASSWORD }} + + - name: dspace basic command + run: | + export DNAME=dspace$INSTANCE + docker logs -n 50 $DNAME + + echo "dspace version:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace version" + + echo "dspace cleanup:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace cleanup -v" + + echo "dspace reindex solr:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace index-discovery -b" + + echo "dspace reindex OAI-PMH:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace oai import -c" + + echo "dspace checker:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace checker -v -l" + + - name: dspace healthcheck + run: | + export DNAME=dspace$INSTANCE + echo "dspace healthcheck:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace healthcheck -v" + import-8: runs-on: dspace-dep-1 if: inputs.IMPORT From 1b769b9bfd1727cc00e1e90fbf505352aa99ca3f Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:44:02 +0100 Subject: [PATCH 02/23] UFAL/Removed unused github docker registry (#1212) --- .github/workflows/build.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d08eaf48708..249b4d84d78 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,9 +49,6 @@ jobs: NODE_OPTIONS: '--max-old-space-size=4096' # Project name to use when running "docker compose" prior to e2e tests COMPOSE_PROJECT_NAME: 'ci' - # Docker Registry to use for Docker compose scripts below. - # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. - DOCKER_REGISTRY: ghcr.io strategy: # Create a matrix of Node versions to test against (in parallel) matrix: @@ -125,14 +122,6 @@ jobs: path: 'coverage/dspace-angular/lcov.info' retention-days: 14 - # Login to our Docker registry, so that we can access private Docker images using "docker compose" below. - - name: Login to ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) From d4d84cb063ac9415b82f0f1561538c689ed96186 Mon Sep 17 00:00:00 2001 From: Paurikova2 <107862249+Paurikova2@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:42:35 +0100 Subject: [PATCH 03/23] internal/Renamed dspace-import to dspace-import-clarin in import action (#1209) * renamed dspace-import to dspace-import-clarin * removed unwanted changes --- .github/actions/import-db/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/import-db/action.yml b/.github/actions/import-db/action.yml index 15c3b355089..0b8984fb5ac 100644 --- a/.github/actions/import-db/action.yml +++ b/.github/actions/import-db/action.yml @@ -30,7 +30,7 @@ runs: - uses: actions/checkout@v4 with: - repository: dataquest-dev/dspace-import + repository: dataquest-dev/dspace-import-clarin ref: 'main' submodules: 'recursive' path: 'dspace-import' @@ -39,7 +39,7 @@ runs: - name: stop and remove containers id: import shell: bash - working-directory: dspace-import/scripts + working-directory: dspace-import-clarin/scripts env: DATADIR: ${{ inputs.DATADIR }} DB5PORT: 15432 From 0ee58b20a6b17105df4e33ec4ca914afbc668e6f Mon Sep 17 00:00:00 2001 From: Paurikova2 <107862249+Paurikova2@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:55:17 +0100 Subject: [PATCH 04/23] UFAL/Editing similar process parameters (#1195) * Preserve parameters when creating similar process * Extract duplicate deep copy logic into helper * Fix redundant parameter logic prevent empty accumulation --- .../form/process-form.component.html | 2 +- .../form/process-form.component.ts | 13 +++++++++ .../process-parameters.component.ts | 29 +++++++++++++++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/app/process-page/form/process-form.component.html b/src/app/process-page/form/process-form.component.html index 211129489e5..6d1cfa4478d 100644 --- a/src/app/process-page/form/process-form.component.html +++ b/src/app/process-page/form/process-form.component.html @@ -5,7 +5,7 @@

- + {{ 'process.new.cancel' | translate }} diff --git a/src/app/process-page/form/process-form.component.ts b/src/app/process-page/form/process-form.component.ts index 70eb3160a8e..7b25acf6d4a 100644 --- a/src/app/process-page/form/process-form.component.ts +++ b/src/app/process-page/form/process-form.component.ts @@ -65,6 +65,19 @@ export class ProcessFormComponent implements OnInit { this.process = new Process(); } + /** + * Handle script selection, preserving existing parameters if creating from existing process + * @param script The selected script + */ + onScriptSelect(script: Script): void { + this.selectedScript = script; + // Clear parameters if we're not creating from an existing process + // (i.e., when no existing parameters or when switching to a different script) + if (!this.parameters) { + this.parameters = []; + } + } + /** * Validates the form, sets the parameters to correct values and invokes the script with the correct parameters * @param form diff --git a/src/app/process-page/form/process-parameters/process-parameters.component.ts b/src/app/process-page/form/process-parameters/process-parameters.component.ts index 85b59f76447..4eab7879027 100644 --- a/src/app/process-page/form/process-parameters/process-parameters.component.ts +++ b/src/app/process-page/form/process-parameters/process-parameters.component.ts @@ -41,7 +41,8 @@ export class ProcessParametersComponent implements OnChanges { ngOnInit() { if (hasValue(this.initialParams)) { - this.parameterValues = this.initialParams; + // Create deep copy to avoid reference issues + this.parameterValues = this.deepCopyParameters(this.initialParams); } } @@ -51,7 +52,17 @@ export class ProcessParametersComponent implements OnChanges { */ ngOnChanges(changes: SimpleChanges): void { if (changes.script) { - this.initParameters(); + // Only reset parameters if we don't have initial parameters to preserve + if (!hasValue(this.initialParams)) { + this.initParameters(); + } else { + // If we have initial parameters, preserve them with deep copy + this.parameterValues = this.deepCopyParameters(this.initialParams); + // Only add an empty parameter if the list is empty or doesn't have a trailing empty parameter + if (this.parameterValues.length === 0 || hasValue(this.parameterValues[this.parameterValues.length - 1].name)) { + this.addParameter(); + } + } } } @@ -61,7 +72,8 @@ export class ProcessParametersComponent implements OnChanges { */ initParameters() { if (hasValue(this.initialParams)) { - this.parameterValues = this.initialParams; + // Create deep copy to avoid reference issues + this.parameterValues = this.deepCopyParameters(this.initialParams); } else { this.parameterValues = []; this.initializeParameter(); @@ -111,4 +123,15 @@ export class ProcessParametersComponent implements OnChanges { addParameter() { this.parameterValues = [...this.parameterValues, new ProcessParameter()]; } + + /** + * Creates a deep copy of ProcessParameter array to avoid reference issues + * @param params The parameters to copy + * @returns A new array with copied ProcessParameter instances + */ + private deepCopyParameters(params: ProcessParameter[]): ProcessParameter[] { + return params.map(param => + Object.assign(new ProcessParameter(), { name: param.name, value: param.value }) + ); + } } From 2f146949a57e5f36e5791e353c67a9b3435d99bd Mon Sep 17 00:00:00 2001 From: Kasinhou <129340513+Kasinhou@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:56:29 +0100 Subject: [PATCH 05/23] UFAL/Display all versions in version history (#1213) * Override pagesize and display all versions * Changed hardcoded variable --------- Co-authored-by: Matus Kasak --- .../clarin-item-versions-field.component.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/simple/field-components/clarin-item-versions-field/clarin-item-versions-field.component.ts b/src/app/item-page/simple/field-components/clarin-item-versions-field/clarin-item-versions-field.component.ts index 5ed269c5988..d0507103c12 100644 --- a/src/app/item-page/simple/field-components/clarin-item-versions-field/clarin-item-versions-field.component.ts +++ b/src/app/item-page/simple/field-components/clarin-item-versions-field/clarin-item-versions-field.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable, of, combineLatest } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, shareReplay } from 'rxjs/operators'; import { ItemVersionsComponent } from '../../../versions/item-versions.component'; import { Item } from '../../../../core/shared/item.model'; import { Version } from '../../../../core/shared/version.model'; @@ -40,6 +40,11 @@ interface EnhancedVersionDTO extends VersionDTO { }) export class ClarinItemVersionsFieldComponent extends ItemVersionsComponent implements OnInit { + /** + * Maximum number of versions to fetch at once for the dropdown display. + */ + private readonly MAX_VERSIONS_TO_DISPLAY = 9999; + /** * Icon name for the clarin field */ @@ -62,7 +67,12 @@ export class ClarinItemVersionsFieldComponent extends ItemVersionsComponent impl enhancedVersions$: Observable; ngOnInit(): void { - // Call parent's ngOnInit first to set up all the observables + // Override the parent's pageSize to fetch all versions at once for the dropdown display + this.pageSize = this.MAX_VERSIONS_TO_DISPLAY; + this.options = Object.assign(this.options, { + pageSize: this.pageSize + }); + super.ngOnInit(); // Set up clarin-specific showMetadataValue logic @@ -98,7 +108,8 @@ export class ClarinItemVersionsFieldComponent extends ItemVersionsComponent impl isCurrentVersion: versionDTO.version.id === currentVersionId } as EnhancedVersionDTO; }); - }) + }), + shareReplay(1) // Cache the result to prevent duplicate requests ); } else { // Fallback: check if isAdmin$ is available, otherwise hide the component From aac563ea9706d39bedf080204b43b7347256b6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Ko=C5=A1arko?= Date: Thu, 19 Feb 2026 09:00:06 +0100 Subject: [PATCH 06/23] UFAL/Missing translation for new curation task (#1208) (cherry picked from commit 5465283b9bac58d8af9513df00af90e7a0ba9baa) --- src/assets/i18n/cs.json5 | 5 ++++- src/assets/i18n/en.json5 | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 7f513b4f918..635847a464a 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -2183,6 +2183,9 @@ // "curation-task.task.registerdoi.label": "Register DOI", "curation-task.task.registerdoi.label": "Zaregistrujte DOI", + // "curation-task.task.metadataqa.label": "Metadata QA", + "curation-task.task.metadataqa.label": "Metadata QA", + // "curation.form.task-select.label": "Task:", "curation.form.task-select.label": "Úloha:", @@ -10068,4 +10071,4 @@ "statistics.views-downloads.view-button": "Statistiky", -} \ No newline at end of file +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0b384eafc3a..77e9cb4497d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1455,6 +1455,8 @@ "curation-task.task.registerdoi.label": "Register DOI", + "curation-task.task.metadataqa.label": "Metadata QA", + "curation.form.task-select.label": "Task:", "curation.form.submit": "Start", From 0434ff9be5d854aa46c6d279a3869713211c433a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Ko=C5=A1arko?= Date: Thu, 19 Feb 2026 11:57:04 +0100 Subject: [PATCH 07/23] UFAL/We expect (in metadata) the path to be "licence" (#1193) (cherry picked from commit 21521d1e16dbb7650b9dddeff5a916fdf142e200) (cherry picked from commit c296d8771ca0ad2e82d358ac12c031a1e3488f7d) --- .../clarin-license-agreement-page.component.ts | 2 +- .../cs/{szn-dataset-license.html => szn-dataset-licence.html} | 0 .../{szn-dataset-license.html => szn-dataset-licence.html} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/static-files/cs/{szn-dataset-license.html => szn-dataset-licence.html} (100%) rename src/static-files/{szn-dataset-license.html => szn-dataset-licence.html} (100%) diff --git a/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts b/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts index fb66d19ea98..38636514dcf 100644 --- a/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts +++ b/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts @@ -115,7 +115,7 @@ export class ClarinLicenseAgreementPageComponent implements OnInit { /** * The path to the Seznam dataset license content. */ - LICENSE_PATH_SEZNAM_CZ = 'szn-dataset-license.html'; + LICENSE_PATH_SEZNAM_CZ = 'szn-dataset-licence.html'; /** * The content of the Seznam dataset license. Fetch from the static file. diff --git a/src/static-files/cs/szn-dataset-license.html b/src/static-files/cs/szn-dataset-licence.html similarity index 100% rename from src/static-files/cs/szn-dataset-license.html rename to src/static-files/cs/szn-dataset-licence.html diff --git a/src/static-files/szn-dataset-license.html b/src/static-files/szn-dataset-licence.html similarity index 100% rename from src/static-files/szn-dataset-license.html rename to src/static-files/szn-dataset-licence.html From 99637f023dd63a66057512c32c2577fc992160e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Ko=C5=A1arko?= Date: Thu, 19 Feb 2026 12:00:51 +0100 Subject: [PATCH 08/23] UFAL/Rewrite OAI links in static page HTML with rest.baseUrl (ufal/dspace-angular#90) (#1189) * Rewrite OAI links in static page HTML with rest.baseUrl Updated StaticPageComponent to rewrite OAI links in loaded HTML content to use the configured rest.baseUrl, ensuring correct API endpoint references. Added comprehensive tests to verify link rewriting, handling of missing baseUrl, avoidance of double slashes, and cases with no OAI links. * Remove unused ComponentFixture import in test Cleaned up the static-page.component.spec.ts file by removing the unused ComponentFixture import to improve code clarity. * Fix OAI URL construction and improve test coverage Corrects the construction of the OAI URL in StaticPageComponent to avoid double slashes by removing the extra slash in the base URL. Also updates the unit test to properly instantiate the component and check its creation. (cherry picked from commit cb86d0778d8b5808d75b95358380a0012e79af4f) Co-authored-by: Amad Ul Hassan --- .../static-page/static-page.component.spec.ts | 90 ++++++++++++++----- src/app/static-page/static-page.component.ts | 6 +- 2 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/app/static-page/static-page.component.spec.ts b/src/app/static-page/static-page.component.spec.ts index 1ad4e607c3c..0c461042381 100644 --- a/src/app/static-page/static-page.component.spec.ts +++ b/src/app/static-page/static-page.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { StaticPageComponent } from './static-page.component'; import { HtmlContentService } from '../shared/html-content.service'; @@ -11,27 +11,25 @@ import { environment } from '../../environments/environment'; import { ClarinSafeHtmlPipe } from '../shared/utils/clarin-safehtml.pipe'; describe('StaticPageComponent', () => { - let component: StaticPageComponent; - let fixture: ComponentFixture; - - let htmlContentService: HtmlContentService; - let appConfig: any; - - const htmlContent = '
TEST MESSAGE
'; - - beforeEach(async () => { - htmlContentService = jasmine.createSpyObj('htmlContentService', { - fetchHtmlContent: of(htmlContent), - getHmtlContentByPathAndLocale: Promise.resolve(htmlContent) + async function setupTest(html: string, restBase?: string) { + const htmlContentService = jasmine.createSpyObj('htmlContentService', { + fetchHtmlContent: of(html), + getHmtlContentByPathAndLocale: Promise.resolve(html) }); - appConfig = Object.assign(environment, { + const appConfig = { + ...environment, ui: { + ...(environment as any).ui, namespace: 'testNamespace' + }, + rest: { + ...(environment as any).rest, + baseUrl: restBase } - }); + }; - TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ declarations: [ StaticPageComponent, ClarinSafeHtmlPipe ], imports: [ TranslateModule.forRoot() @@ -41,22 +39,66 @@ describe('StaticPageComponent', () => { { provide: Router, useValue: new RouterMock() }, { provide: APP_CONFIG, useValue: appConfig } ] - }); - - }); + }).compileComponents(); - beforeEach(() => { - fixture = TestBed.createComponent(StaticPageComponent); - component = fixture.componentInstance; - }); + const fixture = TestBed.createComponent(StaticPageComponent); + const component = fixture.componentInstance; + return { fixture, component, htmlContentService }; + } - it('should create', () => { + it('should create', async () => { + const { component } = await setupTest('
test
'); expect(component).toBeTruthy(); }); // Load `TEST MESSAGE` it('should load html file content', async () => { + const { component } = await setupTest('
TEST MESSAGE
'); await component.ngOnInit(); expect(component.htmlContent.value).toBe('
TEST MESSAGE
'); }); + + it('should rewrite OAI link with rest.baseUrl', async () => { + const oaiHtml = 'OAI'; + const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/rest'); + + await component.ngOnInit(); + fixture.detectChanges(); + + const rewritten = 'https://api.example.org/server/oai/request?verb=ListSets'; + expect(component.htmlContent.value).toContain(rewritten); + const anchor = fixture.nativeElement.querySelector('a'); + expect(anchor.getAttribute('href')).toBe(rewritten); + }); + + it('should leave OAI link unchanged when rest.baseUrl is missing', async () => { + const oaiHtml = 'OAI'; + const { fixture, component } = await setupTest(oaiHtml, undefined); + + await component.ngOnInit(); + fixture.detectChanges(); + + expect(component.htmlContent.value).toContain('/server/oai/request?verb=Identify'); + }); + + it('should avoid double slashes when rest.baseUrl ends with slash', async () => { + const oaiHtml = 'OAI'; + const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/rest/'); + + await component.ngOnInit(); + fixture.detectChanges(); + + expect(component.htmlContent.value).toContain('https://api.example.org/server/oai/request?verb=ListRecords'); + expect(component.htmlContent.value).not.toContain('//server'); + }); + + it('should leave content unchanged when no OAI link is present', async () => { + const otherHtml = 'Other'; + const { fixture, component } = await setupTest(otherHtml, 'https://api.example.org/rest'); + + await component.ngOnInit(); + fixture.detectChanges(); + + expect(component.htmlContent.value).toBe(otherHtml); + }); }); diff --git a/src/app/static-page/static-page.component.ts b/src/app/static-page/static-page.component.ts index bb19403a704..aff3eed0aed 100644 --- a/src/app/static-page/static-page.component.ts +++ b/src/app/static-page/static-page.component.ts @@ -28,8 +28,12 @@ export class StaticPageComponent implements OnInit { // Fetch html file name from the url path. `static/some_file.html` this.htmlFileName = this.getHtmlFileName(); - const htmlContent = await this.htmlContentService.getHmtlContentByPathAndLocale(this.htmlFileName); + let htmlContent = await this.htmlContentService.getHmtlContentByPathAndLocale(this.htmlFileName); if (isNotEmpty(htmlContent)) { + const restBase = this.appConfig?.rest?.baseUrl; + const oaiUrl = restBase ? new URL('/server/oai', restBase).href : '/server/oai'; + htmlContent = htmlContent.replace(/href="\/server\/oai/gi, 'href="' + oaiUrl); + this.htmlContent.next(htmlContent); return; } From d6ed9726d94beeba6fd8799f9eb8eb49c5365ac5 Mon Sep 17 00:00:00 2001 From: Kasinhou <129340513+Kasinhou@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:55:19 +0100 Subject: [PATCH 09/23] Update path for dspace-import in action.yml (#1216) Use dspace-import-clarin path instead of dspace-import --- .github/actions/import-db/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/import-db/action.yml b/.github/actions/import-db/action.yml index 0b8984fb5ac..21fab40bdcd 100644 --- a/.github/actions/import-db/action.yml +++ b/.github/actions/import-db/action.yml @@ -33,7 +33,7 @@ runs: repository: dataquest-dev/dspace-import-clarin ref: 'main' submodules: 'recursive' - path: 'dspace-import' + path: 'dspace-import-clarin' - name: stop and remove containers From 6fc73d8ca0475fb6689d3824138683f2366bb76c Mon Sep 17 00:00:00 2001 From: Jozef Misutka <332350+vidiecan@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:41:53 +0100 Subject: [PATCH 10/23] Refactor erase-db action to clean up comments Removed unnecessary comments and improved logging for volume removal. --- .github/actions/erase-db/action.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/actions/erase-db/action.yml b/.github/actions/erase-db/action.yml index 9ec7e828cd1..ec0ad2ce435 100644 --- a/.github/actions/erase-db/action.yml +++ b/.github/actions/erase-db/action.yml @@ -28,9 +28,6 @@ runs: env: NAME: ${{ inputs.NAME }} run: | - # # condition below was found by accident and appears to be useless. Investigate later. - # be sure to have INSTANCE set - # if [[ "x${NAME}" != "dspace-" ]]; then - docker volume rm $(docker volume ls --filter name="${NAME}_" -q) || true - # fi; + echo "Removing volumes for $(docker volume ls --filter name="${NAME}_" -q)" + docker volume rm $(docker volume ls --filter name="${NAME}_" -q) || true From 911bb36817f38e7d7467dc789b64a713e6f37298 Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:00:51 +0100 Subject: [PATCH 11/23] Use `dspace:dspace` permissions for the assetstore - not ubuntu:ubuntu (#1222) * Use `dspace:dspace` permissions for the assetstore - not ubuntu:ubuntu * Change permissions as root --- .github/actions/import-db/action.yml | 1 + build-scripts/run/start.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/actions/import-db/action.yml b/.github/actions/import-db/action.yml index 21fab40bdcd..4e17c221365 100644 --- a/.github/actions/import-db/action.yml +++ b/.github/actions/import-db/action.yml @@ -62,6 +62,7 @@ runs: echo Location of assetstore folder is empty. Not copping assetstore else docker cp ${{ inputs.ASSETSTORE }} dspace${{ inputs.INSTANCE }}:/dspace/ + docker exec -u root dspace${{ inputs.INSTANCE }} chown -R dspace:dspace /dspace/assetstore fi echo "=====" cd ../ diff --git a/build-scripts/run/start.sh b/build-scripts/run/start.sh index 5dfdfc8cd4d..07a57418779 100755 --- a/build-scripts/run/start.sh +++ b/build-scripts/run/start.sh @@ -48,6 +48,7 @@ popd echo "=====" echo "Copy assetstore" docker cp assetstore dspace${INSTANCE}:/dspace/ +docker exec -u root dspace${INSTANCE} chown -R dspace:dspace /dspace/assetstore echo "=====" echo "Finished start.sh" From 1dd112749d01533b2723e5e4941b0a5f153fbf17 Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:23:46 +0100 Subject: [PATCH 12/23] UFAL/Fix: generate correct curl download URLs using backend handle endpoint (#1215) * fix: generate correct curl download URLs using backend handle endpoint Updates the curl command generation to use the new backend endpoint GET /api/core/bitstreams/handle/{prefix}/{suffix}/{filename} instead of the non-existent /api/bitstream/{handle}/{seq}/{filename}. Key changes: - Uses correct backend endpoint path: /core/bitstreams/handle/{handle}/ - Removes unnecessary sequence index from URLs (uses filename only) - Quotes the URL to prevent shell brace expansion - For single file, uses -o with explicit filename Fixes: #1210 * Fixed formatting of the file names * fix: use -o with real filename to avoid percent-encoded names from curl -O curl -O uses the URL path as the saved filename, so percent-encoded characters (e.g. %20, %2B, %28) stay encoded in the output file. Now generates separate 'curl -o realname url' for each file joined with &&, ensuring files are saved with their actual names. * fix: use curl -OJ with brace expansion for compact download command * removed duplicates logic * fix: use curl -o instead of -OJ to fix non-ASCII filenames on Windows curl -J (Content-Disposition) cannot create files with non-ASCII characters on Windows because it interprets the header bytes using the console code page. Changed to curl -o filename url format where the shell passes the filename directly to the OS, correctly handling Unicode on all platforms. Also added tests for UTF-8 filenames and double-quote escaping. * fix: use inline encodeURIComponent instead of encodeRFC3986URIComponent encodeRFC3986URIComponent calls decodeURIComponent first, which throws URIError on filenames containing a literal percent sign (e.g. '100% done.txt') because '%' followed by non-hex chars is not a valid escape sequence. Replaced with inline encodeURIComponent() + parentheses encoding directly on the raw filename. Added test for literal percent sign in filenames. * fix: restore brace expansion {} in curl URL with -o for filenames curl command now uses brace expansion for compact URL: curl -o file1 -o file2 baseUrl{/encoded1,/encoded2} This combines: - {} brace expansion in the URL (compact, one URL for all files) - -o flags with real filenames (handles UTF-8 correctly via shell) * test: add complex filename test (diacritics, plus, hash, unmatched paren) New FE test for 'Media (+)#9) ano' verifying correct URL encoding in brace expansion and real filename in -o flag. * fix: use separate -o url pairs instead of curl brace expansion curl URL globbing ({}) does NOT support per-file -o flags. When using curl -o f1 -o f2 url{/a,/b} curl maps the -o flags to URL arguments, not to globbed expansions, resulting in 'Got more output options than URLs' and only one file saved. Changed to separate -o + URL pairs per file: curl -o file1 url/file1 -o file2 url/file2 Updated all 12 test expectations to match. * feat: show curl command in modal dialog with copy button Replace inline command display with a centered NgbModal (size: lg) that shows the curl command in a scrollable pre block. Includes a copy-to-clipboard button with visual feedback (checkmark + 'Copied!' for 2s). - Added NgbModal injection and openCommandModal()/copyCommand() methods - Removed old isCommandLineVisible toggle and #command-div hover styles - Added i18n keys for en, cs, de (copy/copied/close) - Updated spec to import NgbModalModule * Revert unnecessary changes * Address Copilot review suggestions: accessibility, security, test fixes - Reset canShowCurlDownload at start of generateCurlCommand() - Add aria-labelledby to modal for screen reader accessibility - Add .catch() to navigator.clipboard.writeText() for error handling - Escape dollar signs and backticks in filenames for shell safety - Fix ConfigurationDataService mock to return RemoteData-shaped object - Add tests for canShowCurlDownload reset and shell injection protection * fix: add fakeAsync/tick to CC license test for debounced getCcLicenseLink --------- Co-authored-by: Paurikova2 --- .../clarin-files-section.component.html | 26 ++- .../clarin-files-section.component.scss | 39 +--- .../clarin-files-section.component.spec.ts | 191 ++++++++++++++++-- .../clarin-files-section.component.ts | 41 +++- ...sion-section-cc-licenses.component.spec.ts | 6 +- src/assets/i18n/cs.json5 | 3 + src/assets/i18n/en.json5 | 3 + 7 files changed, 240 insertions(+), 69 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.html b/src/app/item-page/clarin-files-section/clarin-files-section.component.html index 09829decf58..72d393fbd24 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.html +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.html @@ -3,15 +3,33 @@
 {{'item.page.files.head' | translate}}
-   {{'item.page.download.button.command.line' | translate}} -
-
{{ command }}
-
+ + + + + + { let component: ClarinFilesSectionComponent; let fixture: ComponentFixture; let mockRegistryService: any; - let halService: HALEndpointService; + let halService: any; + + const ROOT_HREF = 'http://localhost:8080/server/api'; + + function createMetadataBitstream(name: string, canPreview: boolean = true): MetadataBitstream { + const bs = new MetadataBitstream(); + bs.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe'; + bs.name = name; + bs.description = 'test'; + bs.fileSize = 1024; + bs.checksum = 'abc'; + bs.type = new ResourceType('item'); + bs.fileInfo = []; + bs.format = 'text'; + bs.canPreview = canPreview; + bs._links = { + self: new HALLink(), + schema: new HALLink(), + }; + bs._links.self.href = ''; + bs._links.schema.href = ''; + return bs; + } + // Set up the mock service's getMetadataBitstream method to return a simple stream - const metadatabitstream = new MetadataBitstream(); - metadatabitstream.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe'; - metadatabitstream.name = 'test'; - metadatabitstream.description = 'test'; - metadatabitstream.fileSize = 1024; - metadatabitstream.checksum = 'abc'; - metadatabitstream.type = new ResourceType('item'); - metadatabitstream.fileInfo = []; - metadatabitstream.format = 'text'; - metadatabitstream.canPreview = false; - metadatabitstream._links = { - self: new HALLink(), - schema: new HALLink(), - }; - - metadatabitstream._links.self.href = ''; - metadatabitstream._links.schema.href = ''; + const metadatabitstream = createMetadataBitstream('test', false); const metadataBitstreams: MetadataBitstream[] = [metadatabitstream]; const bitstreamStream = new BehaviorSubject(metadataBitstreams); @@ -55,7 +63,7 @@ describe('ClarinFilesSectionComponent', () => { }); const configurationServiceSpy = jasmine.createSpyObj('configurationService', { - findByPropertyName: of('123456'), + findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['123456'] }), }); beforeEach(async () => { @@ -63,12 +71,15 @@ describe('ClarinFilesSectionComponent', () => { 'getMetadataBitstream': of(bitstreamStream) } ); - halService = Object.assign(new HALEndpointServiceStub('some url')); + halService = Object.assign(new HALEndpointServiceStub('some url'), { + getRootHref: () => ROOT_HREF + }); await TestBed.configureTestingModule({ declarations: [ ClarinFilesSectionComponent ], imports: [ - TranslateModule.forRoot() + TranslateModule.forRoot(), + NgbModalModule, ], providers: [ { provide: RegistryService, useValue: mockRegistryService }, @@ -88,4 +99,142 @@ describe('ClarinFilesSectionComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('generateCurlCommand', () => { + const BASE = `${ROOT_HREF}/core/bitstreams/handle`; + + it('should generate a curl command for a single file', () => { + component.itemHandle = '123456789/1'; + component.listOfFiles.next([createMetadataBitstream('simple.txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "simple.txt" "${BASE}/123456789/1/simple.txt"` + ); + }); + + it('should generate a curl command for multiple files', () => { + component.itemHandle = '123456789/2'; + component.listOfFiles.next([ + createMetadataBitstream('file1.txt'), + createMetadataBitstream('file2.txt'), + ]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "file1.txt" "${BASE}/123456789/2/file1.txt" ` + + `-o "file2.txt" "${BASE}/123456789/2/file2.txt"` + ); + }); + + it('should percent-encode spaces in URL but keep real name in -o', () => { + component.itemHandle = '123456789/3'; + component.listOfFiles.next([createMetadataBitstream('my file.txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "my file.txt" "${BASE}/123456789/3/my%20file.txt"` + ); + }); + + it('should percent-encode parentheses in URL but keep real name in -o', () => { + component.itemHandle = '123456789/4'; + component.listOfFiles.next([createMetadataBitstream('logo (2).png')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "logo (2).png" "${BASE}/123456789/4/logo%20%282%29.png"` + ); + }); + + it('should percent-encode plus signs in URL', () => { + component.itemHandle = '123456789/5'; + component.listOfFiles.next([createMetadataBitstream('dtq+logo.png')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "dtq+logo.png" "${BASE}/123456789/5/dtq%2Blogo.png"` + ); + }); + + it('should handle mixed special characters in multiple files', () => { + component.itemHandle = '123456789/6'; + component.listOfFiles.next([ + createMetadataBitstream('dtq+logo (2).png'), + createMetadataBitstream('Screenshot 1.png'), + ]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "dtq+logo (2).png" "${BASE}/123456789/6/dtq%2Blogo%20%282%29.png" ` + + `-o "Screenshot 1.png" "${BASE}/123456789/6/Screenshot%201.png"` + ); + }); + + it('should preserve UTF-8 characters in -o filename and encode in URL', () => { + component.itemHandle = '123456789/9'; + component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (3).jfif')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "M\u00e9di\u00e1 (3).jfif" "${BASE}/123456789/9/M%C3%A9di%C3%A1%20%283%29.jfif"` + ); + }); + + it('should escape double quotes in filenames', () => { + component.itemHandle = '123456789/10'; + component.listOfFiles.next([createMetadataBitstream('file "quoted".txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "file \\"quoted\\".txt" "${BASE}/123456789/10/file%20%22quoted%22.txt"` + ); + }); + + it('should set canShowCurlDownload to true when any file canPreview', () => { + component.canShowCurlDownload = false; + component.itemHandle = '123456789/7'; + component.listOfFiles.next([createMetadataBitstream('file.txt', true)]); + component.generateCurlCommand(); + expect(component.canShowCurlDownload).toBeTrue(); + }); + + it('should not set canShowCurlDownload for non-previewable files', () => { + component.canShowCurlDownload = false; + component.itemHandle = '123456789/8'; + component.listOfFiles.next([createMetadataBitstream('file.txt', false)]); + component.generateCurlCommand(); + expect(component.canShowCurlDownload).toBeFalse(); + }); + + it('should handle filenames containing a literal percent sign', () => { + component.itemHandle = '123456789/11'; + component.listOfFiles.next([createMetadataBitstream('100% done.txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "100% done.txt" "${BASE}/123456789/11/100%25%20done.txt"` + ); + }); + + it('should handle complex filename with diacritics, plus, hash, and unmatched paren', () => { + component.itemHandle = '123456789/12'; + component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (+)#9) ano')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "M\u00e9di\u00e1 (+)#9) ano" "${BASE}/123456789/12/M%C3%A9di%C3%A1%20%28%2B%29%239%29%20ano"` + ); + }); + + it('should reset canShowCurlDownload when called again with non-previewable files', () => { + component.itemHandle = '123456789/13'; + component.listOfFiles.next([createMetadataBitstream('file.txt', true)]); + component.generateCurlCommand(); + expect(component.canShowCurlDownload).toBeTrue(); + // Now call again with non-previewable files + component.listOfFiles.next([createMetadataBitstream('file.txt', false)]); + component.generateCurlCommand(); + expect(component.canShowCurlDownload).toBeFalse(); + }); + + it('should escape dollar signs and backticks in filenames for shell safety', () => { + component.itemHandle = '123456789/14'; + component.listOfFiles.next([createMetadataBitstream('price$100.txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "price\\$100.txt" "${BASE}/123456789/14/price%24100.txt"` + ); + }); + }); }); diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 08edfe7ac55..879c4b889c5 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -8,6 +8,7 @@ import { Router } from '@angular/router'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { BehaviorSubject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'ds-clarin-files-section', @@ -29,9 +30,9 @@ export class ClarinFilesSectionComponent implements OnInit { canShowCurlDownload = false; /** - * If download by command button is click, the command line will be shown + * Whether the command was recently copied to clipboard */ - isCommandLineVisible = false; + commandCopied = false; /** * command for the download command feature @@ -75,7 +76,8 @@ export class ClarinFilesSectionComponent implements OnInit { constructor(protected registryService: RegistryService, protected router: Router, protected halService: HALEndpointService, - protected configurationService: ConfigurationDataService) { + protected configurationService: ConfigurationDataService, + protected modalService: NgbModal) { } ngOnInit(): void { @@ -90,8 +92,19 @@ export class ClarinFilesSectionComponent implements OnInit { this.loadDownloadZipConfigProperties(); } - setCommandline() { - this.isCommandLineVisible = !this.isCommandLineVisible; + openCommandModal(content: any) { + this.commandCopied = false; + this.modalService.open(content, { size: 'lg', centered: true, ariaLabelledBy: 'commandModalTitle' }); + } + + copyCommand() { + navigator.clipboard.writeText(this.command).then(() => { + this.commandCopied = true; + setTimeout(() => this.commandCopied = false, 2000); + }).catch(() => { + // Fallback: clipboard API may be unavailable (non-HTTPS, denied permissions) + this.commandCopied = false; + }); } downloadFiles() { @@ -99,6 +112,7 @@ export class ClarinFilesSectionComponent implements OnInit { } generateCurlCommand() { + this.canShowCurlDownload = false; const fileNames = this.listOfFiles.value.map((file: MetadataBitstream) => { if (file.canPreview) { this.canShowCurlDownload = true; @@ -107,10 +121,19 @@ export class ClarinFilesSectionComponent implements OnInit { return file.name; }); - // Generate curl command for individual bitstream downloads - const baseUrl = `${this.halService.getRootHref()}/bitstream/${this.itemHandle}`; - const fileNamesFormatted = fileNames.map((fileName, index) => `/${index}/${fileName}`).join(','); - this.command = `curl -O ${baseUrl}{${fileNamesFormatted}}`; + // Generate curl command with -o "filename" "url" pairs for each file. + // Each file needs its own -o + URL pair because curl URL globbing ({}) + // does NOT support per-file -o flags (multiple -o with {} results in + // "Got more output options than URLs" and only the first file is saved). + // Using -o lets the shell pass the real filename (including UTF-8) directly. + const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; + const parts = fileNames.map(name => { + const encodedName = encodeURIComponent(name) + .replace(/[()]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); + const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`'); + return `-o "${safeName}" "${baseUrl}/${encodedName}"`; + }); + this.command = `curl ${parts.join(' ')}`; } loadDownloadZipConfigProperties() { diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts index dc9bf3c028e..52d2c7481c1 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts @@ -236,11 +236,13 @@ describe('SubmissionSectionCcLicensesComponent', () => { describe('when all options have a value selected', () => { - beforeEach(() => { + beforeEach(fakeAsync(() => { component.selectOption(ccLicence, ccLicence.fields[0], ccLicence.fields[0].enums[1]); component.selectOption(ccLicence, ccLicence.fields[1], ccLicence.fields[1].enums[0]); fixture.detectChanges(); - }); + tick(300); // Wait for debounceTime(300) in ccLicenseLink$ pipeline + fixture.detectChanges(); + })); it('should call the submission cc licenses data service getCcLicenseLink method', () => { expect(submissionCcLicenseUrlDataService.getCcLicenseLink).toHaveBeenCalledWith( diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 635847a464a..937c3706e2a 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -9203,6 +9203,9 @@ // "item.page.download.button.command.line": "Download instructions for command line", "item.page.download.button.command.line": "Instrukce pro stažení z příkazové řádky", + "item.page.download.command.copy": "Kopírovat", + "item.page.download.command.copied": "Zkopírováno!", + "item.page.download.command.close": "Zavřít", // "item.page.download.button.all.files.zip": "Download all files in item", "item.page.download.button.all.files.zip": "Stáhnout všechny soubory záznamu", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 77e9cb4497d..f2460aa3969 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6099,6 +6099,9 @@ "item.page.files.head": "Files in this item", "item.page.download.button.command.line": "Download instructions for command line", + "item.page.download.command.copy": "Copy to clipboard", + "item.page.download.command.copied": "Copied!", + "item.page.download.command.close": "Close", "item.page.download.button.all.files.zip": "Download all files in item", From 6ea61f91c3a9cc7a54319ec60633e0d981fd43b4 Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:54:31 +0100 Subject: [PATCH 13/23] UFAL/CURL downloads issue - fix integration test --- .../submission-section-cc-licenses.component.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts index 52d2c7481c1..d0b1aaad0df 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts @@ -210,10 +210,12 @@ describe('SubmissionSectionCcLicensesComponent', () => { const ccLicence = submissionCcLicenses[1]; - beforeEach(() => { + beforeEach(fakeAsync(() => { component.selectCcLicense(ccLicence); fixture.detectChanges(); - }); + tick(300); // Flush debounce timer so no real timer leaks into subsequent fakeAsync zones + fixture.detectChanges(); + })); it('should display the selected cc license', () => { expect(component.selectedCcLicense.name).toContain('test license name 2'); From 31adb3161e2dc04d192e8a2dcbecfbaa8161eee8 Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:14:12 +0100 Subject: [PATCH 14/23] UFAL/Fix: preserve namespace in OAI link rewrite on static pages (#1226) (#1227) --- .../static-page/static-page.component.spec.ts | 21 +++++++++++++++---- src/app/static-page/static-page.component.ts | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/app/static-page/static-page.component.spec.ts b/src/app/static-page/static-page.component.spec.ts index 0c461042381..69d65248509 100644 --- a/src/app/static-page/static-page.component.spec.ts +++ b/src/app/static-page/static-page.component.spec.ts @@ -60,7 +60,7 @@ describe('StaticPageComponent', () => { it('should rewrite OAI link with rest.baseUrl', async () => { const oaiHtml = 'OAI'; - const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/rest'); + const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/server'); await component.ngOnInit(); fixture.detectChanges(); @@ -83,18 +83,31 @@ describe('StaticPageComponent', () => { it('should avoid double slashes when rest.baseUrl ends with slash', async () => { const oaiHtml = 'OAI'; - const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/rest/'); + const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/server/'); await component.ngOnInit(); fixture.detectChanges(); expect(component.htmlContent.value).toContain('https://api.example.org/server/oai/request?verb=ListRecords'); - expect(component.htmlContent.value).not.toContain('//server'); + expect(component.htmlContent.value).not.toContain('//oai'); + }); + + it('should include namespace in OAI link when rest.baseUrl has namespace prefix', async () => { + const oaiHtml = 'full list'; + const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/repository/server'); + + await component.ngOnInit(); + fixture.detectChanges(); + + const rewritten = 'https://api.example.org/repository/server/oai/request?verb=ListMetadataFormats'; + expect(component.htmlContent.value).toContain(rewritten); + const anchor = fixture.nativeElement.querySelector('a'); + expect(anchor.getAttribute('href')).toBe(rewritten); }); it('should leave content unchanged when no OAI link is present', async () => { const otherHtml = 'Other'; - const { fixture, component } = await setupTest(otherHtml, 'https://api.example.org/rest'); + const { fixture, component } = await setupTest(otherHtml, 'https://api.example.org/server'); await component.ngOnInit(); fixture.detectChanges(); diff --git a/src/app/static-page/static-page.component.ts b/src/app/static-page/static-page.component.ts index aff3eed0aed..018ed5d8cf5 100644 --- a/src/app/static-page/static-page.component.ts +++ b/src/app/static-page/static-page.component.ts @@ -31,7 +31,7 @@ export class StaticPageComponent implements OnInit { let htmlContent = await this.htmlContentService.getHmtlContentByPathAndLocale(this.htmlFileName); if (isNotEmpty(htmlContent)) { const restBase = this.appConfig?.rest?.baseUrl; - const oaiUrl = restBase ? new URL('/server/oai', restBase).href : '/server/oai'; + const oaiUrl = restBase ? restBase.replace(/\/+$/, '') + '/oai' : '/server/oai'; htmlContent = htmlContent.replace(/href="\/server\/oai/gi, 'href="' + oaiUrl); this.htmlContent.next(htmlContent); From 6628aaf493b1fcd6a01e2ba10bc66e8740a43458 Mon Sep 17 00:00:00 2001 From: Kasinhou <129340513+Kasinhou@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:59:49 +0100 Subject: [PATCH 15/23] UFAL/Resolve duplicate HTML element IDs across pages (#1221) * fix: resolve duplicate HTML element IDs across pages * fix: use [attr.*] binding syntax for aria-* attributes in templates * fix: revert dynamic form ID changes to fix e2e test failures Reverts DsDynamicFormLayoutService and all form template changes that were appending _N suffixes to form element IDs, breaking Cypress e2e selectors like input#dc_title and label[for=local_hasCMDI]. Only static HTML duplicate ID fixes are retained (pagination, ds-select, epeople-registry, etc.) along with aria-* attribute binding fixes. * fix: address Copilot review comments - sanitize bundleName for IDs, remove invalid file-tree ID, fix conditional aria-describedby --------- Co-authored-by: Matus Kasak --- .../epeople-registry.component.html | 2 +- ...larin-ref-featured-services.component.html | 6 ++-- .../item-edit-bitstream-bundle.component.html | 28 +++++++++---------- .../item-edit-bitstream-bundle.component.ts | 7 +++++ .../clarin-sponsor-item-field.component.html | 8 +++--- ...larin-sponsor-item-field.component.spec.ts | 12 ++++---- .../file-tree-view.component.html | 2 +- .../file-tree-view.component.spec.ts | 2 +- .../shared/ds-select/ds-select.component.html | 10 +++---- .../shared/ds-select/ds-select.component.ts | 7 +++++ ...-search-result-list-element.component.html | 2 +- .../pagination/pagination.component.html | 16 +++++------ .../pagination/pagination.component.spec.ts | 12 ++++---- .../section-license.component.html | 2 +- 14 files changed, 65 insertions(+), 51 deletions(-) diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index c2f3c0db047..d19230e5e48 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -26,7 +26,7 @@
- + diff --git a/src/app/item-page/clarin-ref-featured-services/clarin-ref-featured-services.component.html b/src/app/item-page/clarin-ref-featured-services/clarin-ref-featured-services.component.html index a4adbef4397..1f019c9cad9 100644 --- a/src/app/item-page/clarin-ref-featured-services/clarin-ref-featured-services.component.html +++ b/src/app/item-page/clarin-ref-featured-services/clarin-ref-featured-services.component.html @@ -13,7 +13,7 @@