Skip to content

Dtq dev 9 base - #193

Open
vidiecan wants to merge 110 commits into
ufal:clarin-v9from
dataquest-dev:dtq-dev-9-base
Open

vidiecan wants to merge 110 commits into
ufal:clarin-v9from
dataquest-dev:dtq-dev-9-base

Conversation

@vidiecan

Copy link
Copy Markdown

PoC of v9 migration of clarin-dspace with all features and functionality.

milanmajchrak and others added 30 commits July 16, 2026 09:05
* Port CLARIN frontend image assets (mime icons, flags, branding)

First tranche of the CLARIN-DSpace 7.6.x -> 9.3 dspace-angular port.
Adds CLARIN/LINDAT image assets that are net-new vs vanilla 9.3 (none of these
dirs exist upstream), so this is purely additive and build-safe:
- src/assets/images/mime/   (file-type icons for item/bitstream display)
- src/assets/images/flags/  (country/language flags)
- src/assets/images/item-types/, footer/, static-pages/
- LINDAT branding: lindat-logo-new-sm.png, lindat_color_line.png, target.png,
  info.png, spinning.gif

Feature modules that consume these assets follow in subsequent tranches
(see CLARIN_DSPACE_V9_PROGRESS.md §6e for the frontend plan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN frontend core layer (models + data services) to angular 9

Second tranche of the dspace-angular 7.6.x -> 9.3 CLARIN port: the core data
layer that the feature modules depend on. Type-checks clean (tsc) and passes
lint (0 errors) on v9.

- shared/clarin: models, resource-types, serializers, constants for the license
  framework (ClarinLicense/Label/ResourceMapping/CLRUA), user metadata/registration,
  verification token, matomo report subscription, featured services, bitstream auth.
- core/data + core/data/clarin: data services for the above + handle / epic-handle /
  metadata-bitstream / metadata-value / bitstream-checksum.
- core/handle, core/epicHandle, core/metadata: handle + epic-handle + metadata models,
  resource-types and serializers.

v9 migration applied:
- @dataService decorator import repointed from core/data/base/data-service.decorator
  to core/cache/builders/build-decorators (moved upstream in v8/9; still used for HAL
  resolution) - decorator kept.
- eslint --fix for v9 style (import order etc.).

Deferred (need the clarin-item-box-view feature): bitstream-url-serializer +
shared/clarin-shared-util. Feature modules (clarin-licenses, handle-page, login,
share-submission, ...) follow in subsequent tranches. See CLARIN_DSPACE_V9_PROGRESS.md §6e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN clarin-licenses feature module to angular 9 (standalone)

Third FE tranche: the flagship CLARIN license admin/listing feature, migrated
from the 7.x NgModule pattern to v9 standalone components. ng build passes
(module is lazy-loaded so the whole chain incl. the core layer is validated);
lint clean (0 errors).

- 5 components -> standalone (clarin-license-page, clarin-license-table,
  clarin-all-licenses-page, define-license-form, define-license-label-form) with
  explicit imports (CommonModule, TranslateModule, FormsModule/ReactiveFormsModule,
  NgbDropdownModule, ThemedLoadingComponent, PaginationComponent + the pipes below).
- 6 custom CLARIN pipes ported + made standalone (shared/utils): char-to-end (dsAddChar),
  clarin-extended-license, clarin-license-checked, clarin-license-label-radio-value,
  clarin-license-required-info(+checked).
- clarin-license-routes.ts (replaces clarin-license.module.ts + clarin-license-routing.module.ts)
  using v9 function resolver/guard (i18nBreadcrumbResolver / siteAdministratorGuard).
- LICENSES_MODULE_PATH / LICENSES_MANAGE_TABLE_PATH added to app-routing-paths.ts and the
  /licenses lazy route wired into app-routes.ts.

Consumes the core layer from the previous tranche. See CLARIN_DSPACE_V9_PROGRESS.md §6e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix clarin-licenses templates for v9 lint gate (control-flow, dsBtnDisabled, ===)

Follow-up to the clarin-licenses port: the CI gate `ng lint --quiet` also lints
templates (which a scoped .ts-only eslint run missed). Applied the v9 template
requirements so both `ng build` and full `ng lint --quiet` pass (0 errors):

- *ngIf/*ngFor -> @if/@for via @angular/core:control-flow migration
  (@angular-eslint/template/prefer-control-flow).
- == -> === in templates (@angular-eslint/template/eqeqeq).
- disabled/[disabled] on <button> -> [dsBtnDisabled] + BtnDisabledDirective import
  (dspace-angular-html/no-disabled-attribute-on-button).
- Re-sorted standalone imports / import statements (simple-import-sort,
  sort-standalone-imports).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN handle-page (handle management) to angular 9 (standalone)

FE tranche 4: admin handle table + new/edit/change-prefix pages, migrated to v9
standalone. ng build + full ng lint --quiet both clean.

- 6 components -> standalone (handle-page, handle-table, new/edit-handle-page,
  change-handle-prefix-page, handle-global-actions) with precise template-dep imports
  (CommonModule/TranslateModule/Forms/ReactiveForms/NgbDropdown + ThemedLoadingComponent,
  PaginationComponent, ErrorComponent, BtnDisabledDirective, sibling components).
- handle-page-routes.ts (replaces handle-page.module.ts + handle-page.routing.module.ts),
  /handle-table lazy route wired into app-routes.ts with siteAdministratorGuard.
- v9 template migration: @if/@for control-flow, === / !==, dsBtnDisabled.

Consumes the handle core data services from FE tranche 2. See CLARIN_DSPACE_V9_PROGRESS.md §6e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN epic-handle (EPIC PID management) to angular 9 (standalone)

FE tranche 5: EPIC PID handle table + prefix + new/edit pages, migrated to v9
standalone. ng build + full ng lint --quiet both clean.

- 5 components -> standalone (epic-handle, epic-handle-table, epic-handle-new/edit/prefix)
  with precise imports (Common/Translate/Forms/ReactiveForms + ThemedLoadingComponent,
  PaginationComponent, VarDirective (*ngVar), BtnDisabledDirective, sibling component).
- epic-handle-routes.ts (replaces epic-handle.module.ts + epic-handle-routing.module.ts),
  /epic-handle-table lazy route wired into app-routes.ts with siteAdministratorGuard.
- v9 migrations: @if/@for control-flow, dsBtnDisabled, and rxjs catchError errors kept as
  `unknown` with `(error as any)` casts at use (smarttools/rxjs/no-implicit-any-catch).

Consumes the epic-handle core data services from FE tranche 2. See CLARIN_DSPACE_V9_PROGRESS.md §6e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN share-submission + change-submitter to angular 9 (standalone)

FE tranche 6: "Sharing a submission" feature (share link page + change-submitter page).
ng build + full ng lint --quiet both clean.

- ShareSubmissionPageComponent + ChangeSubmitterPageComponent -> standalone.
- share-submission-routes.ts (replaces share-submission.module.ts + routing module),
  /share-submission lazy route wired into app-routes.ts with authenticatedGuard.
- v9 fixes: @if control-flow, eqeqeq, replaced stray `chart.js/helpers` isNullOrUndef with
  DSpace hasNoValue, fixed `instanceof Observable<T>` -> `instanceof Observable`.

See CLARIN_DSPACE_V9_PROGRESS.md §6e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN contact-page + static-page to angular 9 (standalone + themed)

FE tranche 7: contact page (themed) + static HTML pages. ng build + full ng lint clean.

- ContactPageComponent + StaticPageComponent -> standalone; ThemedContactPageComponent
  already matches v9 themed pattern (no change needed).
- Ported ClarinSafeHtmlPipe (dsSafeHtml) + HtmlContentService (providedIn:'root'), both
  CLARIN-added and absent in vanilla v9.
- contact-page-routes.ts + static-page-routes.ts (replace NgModules); /contact and /static
  lazy routes wired into app-routes.ts.
- v9 adaptations: ds-themed-loading -> ds-loading, @if control-flow, eqeqeq,
  LocaleService.getCurrentLanguageCode() now Observable<string> (await firstValueFrom),
  catch error cast (error as any).status, REQUEST token moved to ../../express.tokens,
  no-negated-async -> (htmlContent | async)?.length === 0.

See CLARIN_DSPACE_V9_PROGRESS.md §6e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix FE runtime defects found by independent review (DI providers, *ngVar, route)

Resolves CRITICAL/MINOR findings from the independent FE review — runtime bugs that
ng build/ng lint cannot catch (DI is runtime; strictTemplates is off):

- C1: 12 CLARIN core data services had bare @Injectable() (no provider) -> NullInjectorError
  at runtime (clarin-licenses + handle-page were dead). Added @Injectable({ providedIn: 'root' })
  to all (license/label/mapping/user-metadata/user-registration/verification-token/clrua/
  matomo-report/handle/bitstream-checksum/metadata-bitstream/metadata-value data services).
- C2: *ngVar used without importing VarDirective in clarin-license-table, handle-table,
  change-submitter-page -> template alias never assigned, pages rendered no data. Imported VarDirective.
- m2: restored pathMatch:'full' on the contact route.

Deferred-with-reason (documented, not skipped): FE M1 usage-report.model delta (would break vanilla
stats in isolation), C3 admin menu provider, i18n keys. See CLARIN_DSPACE_V9_PROGRESS.md §6g.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN i18n keys into en.json5 (FE tranche 8)

Adds the 643 CLARIN-DSpace (UFAL) English translation keys that v9 lacks (computed as the set
difference of dtq-dev keys minus v9 keys, extracted via the json5 parser so multi-line array values
are preserved). Covers clarin licenses, handle-table, epic-handle, share-submission, change-submitter,
contact/static pages, admin update-config, matomo, refbox, statistics, etc. — so the already-ported
and upcoming CLARIN FE modules render real labels instead of raw keys.

en.json5 re-validated with JSON5.parse (4318 total keys). Other languages fall back to en for these
keys; Czech (cs) translations can be layered later. Appended (en.json5 is not strictly sorted in-file
and no parity/sort test is enforced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN license-contract-page (FE tranche 9, standalone)

Distribution-license display per collection (route /contract). v9 standalone migration:
- standalone component with imports [AsyncPipe, TranslateModule, VarDirective, ErrorComponent,
  ThemedLoadingComponent, PaginationComponent]
- control-flow migration (*ngIf/*ngFor/else -> @if/@for/@else)
- v9 API fixes: ds-themed-loading -> ds-loading (v9 selector); getAuthorizedCollection now takes an
  explicit searchHref param -> pass 'findSubmitAuthorized' (the 7.x default) before followLink('license')
- NgModule/routing.module -> license-contract-page-routes.ts; wired loadChildren at path 'contract'
Validated: npm run build succeeds (0 errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN admin-update-config (FE tranche 10, standalone)

Admin config-file editor UI (route /admin/update-config, site-admin guarded) — pairs with the BE
ConfigFileRestController (admin/configfiles endpoint). v9 standalone migration:
- standalone component imports [DatePipe, DecimalPipe, FormsModule, TranslateModule]
- control-flow migration; AdminUpdateConfigService -> @Injectable({ providedIn: 'root' }) (v9 DI)
- direct route entry in admin-routes.ts with siteAdministratorGuard + i18nBreadcrumbResolver
Validated: npm run build succeeds (0 errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN bitstream-page (license-gated download) + tombstone cluster (FE tranche 11)

Flagship CLARIN feature: license-gated bitstream download. 5 standalone components — download-page
(routes :id/download -> ClarinBitstreamDownloadPageComponent), license-agreement-page (the license
signing form), token-expired, authorization-denied, zip-download-page. Uses ported CLARIN FE services
(ClarinLicenseResourceMappingService, ClarinUserRegistration/MetadataDataService).
Also the tombstone cluster (tombstone + replaced/withdrawn-tombstone) it depends on for HELP_DESK_PROPERTY.

v9 standalone migration: imports[] per component (AsyncPipe/TranslateModule/VarDirective/ClarinSafeHtmlPipe
+ cross-component composition), control-flow migration, ds-themed-loading->ds-loading is N/A here.
v9 fixes: no-unsafe-enum-comparison (downloadStatus string vs RequestEntryState -> cast). Wired the
:id/download route to ClarinBitstreamDownloadPageComponent in bitstream-page-routes.ts.
Validated: npm run build SUCCESS (✔ bundle generation complete, 0 errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN shared foundation + item-display chain (FE tranche 12, standalone)

Bottom-up port of the leaf dependencies for item listings/view:
- foundation: clarin-shared-util (getBaseUrl/secureImageData/loadItemAuthors), clarin-date.service +
  item-identifier.service (@Injectable providedIn:'root'), utils/replace.pipe (dsReplace), AuthorNameLink model
- item-display components (standalone, control-flow): clarin-license-info (license badge/info,
  switch on LicenseType cast to satisfy no-unsafe-enum-comparison), clarin-item-author-preview,
  clarin-item-box-view (composes author-preview + license-info; imports VarDirective/RouterLink/FileSizePipe)
Validated: npm run build SUCCESS + eslint --quiet clean. These unblock the item-page listing/view tranche.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN ref-box cluster (FE tranche 13, standalone)

The item-view citation/featured-services box — pairs with the BE ClarinRefBoxController
(/server/api/core/refbox). 4 standalone components (control-flow migrated):
- clarin-ref-box (composes citation + featured-services)
- clarin-ref-citation (+ clarin-ref-citation-modal for the BibTeX/citation export modal)
- clarin-ref-featured-services (featured service links per item)
Validated: npm run build SUCCESS + eslint --quiet clean. (Wiring ds-clarin-ref-box into the themed
item view is a follow-up vanilla-template change.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN item-field display components (FE tranche 14, standalone)

8 item-view metadata field renderers (control-flow migrated, build + eslint validated):
clarin-generic-item-field (dispatcher composing date/description/identifier/sponsor + item-author-preview)
+ clarin-date/description/identifier/sponsor/collections/files/item-versions field components.
Use the shared foundation from tranche 12 (clarin-shared-util, ClarinDateService, dsReplace) + vanilla
FileDownloadLinkComponent/ThemedLoadingComponent. Validated: npm run build SUCCESS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN cc-license-field + item-page-citation + matomo-subscription-button (FE tranche 15)

3 more item-view components (standalone, control-flow, build+eslint validated):
- creative-commons-license-field (CC license display)
- item-page-citation (citation specific-field)
- matomo-subscription-button (subscribe to monthly Matomo stats; uses the ported FE
  MatomoReportSubscriptionDataService)
Validated: npm run build SUCCESS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN item-license-mapper + access-status-badge + dso-page-subscription-button (FE tranche 16)

3 standalone components (control-flow + build + eslint validated):
- item-license-mapper (edit-item-page admin: map a CLARIN license to an item; uses ClarinLicenseDataService)
- access-status-badge (+ access-status model/resource-type) for item listings
- dso-page-subscription-button (CLARIN subscription button on DSO pages)
Validated: npm run build SUCCESS. (Deferred: clarin-search — extends vanilla SearchComponent + reuses its
full template, needs the whole SearchComponent imports[] replicated.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix FE lint errors across CLARIN tranches 9-16 (ng lint gate)

This session I validated with `npm run build` + targeted `eslint --quiet <ts>` but skipped the full
`ng lint` (lint:nobuild) which lints HTML templates + custom rules — so several tranches failed CI lint.
Comprehensive fix (ng lint now: 0 errors, only pre-existing warnings):
- themed-component-usages: clarin-files-item-field must import ThemedFileDownloadLinkComponent (not the
  raw FileDownloadLinkComponent) — this rule crash was aborting the whole lint
- template eqeqeq (==/!= -> ===/!==) in 7 templates (license-agreement, files/generic-item-field,
  tombstone+withdrawn-tombstone, item-author-preview, item-box-view)
- no-negated-async -> (x | async) !== true (collections-item-field, item-author-preview)
- no-sharereplay: shareReplay(1) -> shareReplay({ bufferSize: 1, refCount: true }) (item-versions-field)
- import sorting/newlines autofixed (ng lint --fix), CLARIN files only
Validated: ng lint -> 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN login outcome pages (FE tranche 17, standalone)

Shibboleth login outcome/registration pages (routed under /login): auth-failed, missing-headers,
duplicate-user, autoregistration. Standalone (control-flow), wired into login-page-routes.ts.
v9-API migration: autoregistration-loader used the removed ComponentFactoryResolver.resolveComponentFactory
-> ViewContainerRef.createComponent(AutoregistrationComponent) directly. Uses ported CLARIN
ClarinVerificationTokenDataService + clarin-shared-util. Validated: npm run build SUCCESS + ng lint
(ts+html) 0 errors (fixed template eqeqeq).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix FE build breakage (dsBtnDisabled directive + login imports) — proper build+lint validation

Root cause: my FE build-check grep matched only `error TS|Module not found` and MISSED Angular
NG8001/NG8002 template-binding errors, and the tranche-15 `ng lint --fix` auto-converted button
`disabled` attributes to `[dsBtnDisabled]` WITHOUT importing BtnDisabledDirective -> build broke.
Fixes:
- admin-update-config + clarin-license-agreement-page: add BtnDisabledDirective to imports[]
- autoregistration.component: was already standalone in 7.x — my script added a 2nd imports[]; merged
  to one, dropped the removed SharedModule + CommonModule (use AsyncPipe/RouterLink/TranslateModule)
- auth-failed-page: add FormsModule (template uses [(ngModel)])
- defer clarin-notice submission section: it uses <ng-toggle> from @nth-cloud/ng-toggle (a CLARIN npm
  dep not yet installed) -> _deferred/ until the dep is added
Validated PROPERLY this time: npm run build -> 0 errors (incl. NG8xxx) + eslint --ext .ts,.html 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add CLARIN-DSpace FE npm dependencies (unblocks charts/toggle/discojuice/form-builder clusters)

Add the CLARIN deps from dtq-dev that v9 lacked: d3 + @types/d3 (views-downloads statistics charts),
chart.js + ng2-charts (chart components), @nth-cloud/ng-toggle (submission/notice toggles), lindat-common
(discojuice/AAI shibboleth navbar), ngx-mask (form input masks), sanitize-html + @types/sanitize-html.
Also pin @popperjs/core (a @ng-bootstrap transitive dep that --legacy-peer-deps otherwise drops).
Installed with --legacy-peer-deps (ng2-charts@4 peer-declares Angular <=16 but builds fine on v9's 17/18).
Validated: npm run build SUCCESS (0 errors). These were the gating prerequisite for the remaining FE
CLARIN feature clusters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* FE: port views/downloads statistics page (d3 charts) + per-item button

CLARIN per-item usage statistics (uses the now-installed d3 dep):
- views-downloads-statistics/ : routed page (chart-drawer.service draws d3 line/area charts of
  yearly->monthly->daily views & downloads; drill-down on click), service fetches from the external
  statistics service (APP_CONFIG.statistics.baseUrl+endpoint)
- views-downloads-statistics-button/ : routerLink button to the page
- route wired in item-page-routes.ts (VIEWS_DOWNLOADS_STATISTICS_PATH) as a child of :id
- StatisticsConfig (src/config/statistics-config.ts) added to AppConfig (optional `statistics?`)
- ItemComponent.hasConfiguredStatistics getter (via inject(APP_CONFIG) to avoid touching the
  widely-extended constructor) gates the button in untyped-item; full-item-page shows it unconditionally
- button embedded in base untyped-item + full-item-page templates AND their themes/custom overrides
  (theme overrides reuse base templateUrl, so they need the import in their own imports[] too)
- also fixed matomo-subscription-button: add BtnDisabledDirective (template had [dsBtnDisabled])
Made all components standalone (imports[] + @if/@for control-flow). Validated: npm run build SUCCESS
(0 errors incl NG8xxx) + eslint --ext .ts,.html 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* FE: port clarin-notice submission section (uses @nth-cloud/ng-toggle)

CLARIN submission "notice" section: shows the deposit-agreement notice with the collection name +
help-desk mailto, and an accept toggle (ng-toggle) the submitter must switch on. Now buildable thanks
to the installed @nth-cloud/ng-toggle dep.
- SubmissionSectionClarinNoticeComponent extends SectionModelComponent (implements getSectionStatus/
  onSectionInit/onSectionDestroy); standalone with imports [AsyncPipe, FormsModule, NgToggleModule,
  TranslateModule]; @if/@for control-flow
- registered via explicit submissionSectionsMap.set(SectionsType.clarinNotice, ...) in
  sections-decorator.ts (v9 style; the 7.x @renderSectionFor decorator dropped to avoid circular import)
- SectionsType += clarinLicense/clarinNotice (values match the BE submission step types)
Validated: npm run build SUCCESS (0 errors) + eslint 0 problems. (Becomes visible once the BE
item-submission.xml wires a clarin-notice step — that BE config is tracked separately.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* FE: port CLARIN/LINDAT top navbar (language toggle + AAI discojuice sign-on)

clarin-navbar-top: the LINDAT-style top bar rendered above the dspace-theme header — EN/CS language
toggle (own setLanguage via LocaleService) and an AAI sign-on badge that DiscoJuice binds to
(#clarin-signon-discojuice), or profile/logout links when authenticated.
- ClarinNavbarTopComponent standalone (imports [RouterLink, TranslateModule], providers
  [ScriptLoaderService]); @if control-flow; eqeqeq fixed
- ScriptLoaderService loads DiscoJuice/AAI/AAIConfig scripts browser-side only (SSR-guarded via
  isPlatformBrowser) on AfterViewInit
- wired into src/themes/dspace/app/header/header.component (.html + imports[]) at the top of the header
- the dtq-dev template's `ds-language-selection` is a CSS class (not a component), so no extra import
Validated: npm run build SUCCESS (0 errors) + eslint 0 problems.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* FE: port CLARIN form-builder custom field types (autocomplete / sponsor / complex / clarin-name)

The CLARIN submission custom input types, now buildable on v9's @ng-dynamic-forms:
- autocomplete: DsDynamicAutocomplete{Model,Component,Service} — typeahead backed by vocabulary /
  custom solr/json sources (handle-title, iso-langs); sponsor variant (openAIRE + metadata) and a
  sponsor scrollable-dropdown variant for the local.sponsor complex input
- clarin-name + complex models/parsers; ds-dynamic-complex.model (SPONSOR_METADATA_NAME etc.)
- registration: ParserType += Autocomplete/Complex/ClarinName (+ parser-factory cases);
  dsDynamicFormControlMapFn maps the AUTOCOMPLETE/SCROLLABLE_DROPDOWN types to the right component
  (sponsor name-prefix picks the sponsor variants)
- FormFieldModel += complexDefinition + autocompleteCustom (@autoserialize)
- field-parser.setVocabularyOptionsInComplexInput + LookupRelationService.getExternalResults ported

v9 API migrations made along the way:
- components made standalone (imports[] for the reused tag/scrollable templates)
- DsDynamicTagComponent.cdr private->protected so the autocomplete subclass can use it; subclasses
  pass cdr/dataServiceMap to super as plain params (base declares them private/protected)
- sponsor scrollable-dropdown super() updated to the v9 6-arg signature (Injector + APP_DATA_SERVICES_MAP)
- ngb-highlight -> span (v9 ngb-highlight now requires result/term inputs)
Validated: npm run build SUCCESS (0 errors) + eslint --ext .ts,.html 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* FE: port BitstreamUrlSerializer (percent-encode /bitstream/ download filenames)

CLARIN custom UrlSerializer that RFC3986-encodes the filename segment of /bitstream/.. URLs so
download links with special characters resolve correctly. Registered as { provide: UrlSerializer,
useClass: BitstreamUrlSerializer } in app.config.ts (v9 standalone equivalent of the 7.x app.module
provider). Uses the already-ported encodeRFC3986URIComponent.
Validated: npm run build SUCCESS (0 errors) + eslint 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* FE: port CLARIN license submission sections (clarin-license-resource + clarin-license-distribution)

The core CLARIN submission license flow:
- clarin-license-resource/section-license: the "resource license" step — loads CLARIN licenses from
  the BE (ClarinLicenseDataService), renders a license selector (License4Selector), validates the
  chosen license is CLARIN-supported, patches the workspace item; standalone [CommonModule,
  TranslateModule], own ufal-theme.css; registered as SectionsType.clarinLicense
- clarin-license-distribution: overrides the vanilla License (distribution) step with an ng-toggle
  accept control + a link to the distribution-license contract page; standalone [CommonModule,
  FormsModule, NgToggleModule, RouterLink, TranslateModule]; registered for SectionsType.License
  (replaces SubmissionSectionLicenseComponent in the sections map)
- both registered via submissionSectionsMap.set() (v9; @renderSectionFor decorator dropped)
- app-routing-paths += CONTRACT_PAGE_MODULE_PATH + getLicenseContractPagePath()
Validated: npm run build SUCCESS (0 errors) + eslint 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix FE CI: repair package-lock so `npm clean-install` passes again

Root cause of the red `tests (20.x/22.x)` checks: my earlier `npm install --legacy-peer-deps` to add
the CLARIN deps corrupted package-lock.json — it flattened the vanilla mirador/react-intersection-observer
react peer nesting and dropped transitives (chokidar, readdirp, clsx, yaml), so CI's `npm clean-install`
failed with "Invalid/Missing from lock file". It also blindly downgraded ngx-mask 16->13 and added
unused deps.

Fix (verified locally with `npm clean-install` == CI's command, exit 0):
- restore ngx-mask to the vanilla ^16.0.0 (the 5 in-tree users need v16; v13 was a stray downgrade)
- drop unused deps never imported anywhere: chart.js, ng2-charts, lindat-common, sanitize-html
  (+ @types/sanitize-html, @types/d3-dispatch). ng2-charts@4 was also the only Angular-18 peer conflict.
- keep only what's actually used: d3 + @types/d3 (charts), @nth-cloud/ng-toggle (toggles, no peers),
  @popperjs/core (@ng-bootstrap transitive pin)
- regenerate the lock by adding these onto the WORKING vanilla lock via `npm install --package-lock-only`
  (no --legacy-peer-deps) so the mirador react-peer nesting is preserved
Validated: npm clean-install -> 0, npm run build -> 0 (Build at: ... 0 errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* FE: make item.component APP_CONFIG injection optional (unbreak item-type unit tests)

inject(APP_CONFIG) without { optional: true } throws NullInjectorError in any item-type component
spec that doesn't provide APP_CONFIG (untyped-item.spec et al.), failing test:headless. Use
inject(APP_CONFIG, { optional: true }) + null-safe access in hasConfiguredStatistics — the app still
provides APP_CONFIG, specs without it just get null (button hidden) instead of crashing.
Validated: npm run build:prod -> 0 (CI's production build gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix FE circular dependencies (check-circ-deps CI step)

The `Check for circular dependencies` (madge) CI step failed: my CLARIN ports created 3 cycles by
defining constants inside component files that other modules import:
- HELP_DESK_PROPERTY was defined in tombstone.component but imported by replaced-/withdrawn-tombstone
  children -> cycle. Extracted to tombstone.constants.ts (9 importers rewired).
- DOI_METADATA_FIELD/HANDLE_METADATA_FIELD were defined in clarin-generic-item-field.component but
  DOI is imported by shared/item-identifier.service (which the sibling clarin-identifier-item-field uses)
  -> cycle. Extracted to clarin-generic-item-field.constants.ts.
Validated: madge with CI's exact --exclude pattern -> "No circular dependency found!" (exit 0);
npm run build -> 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Force-add ufal-theme.css (gitignored *.css) so the prod build finds it

The FE `Run build` (build:prod) CI step failed: section-license.component.scss does
`@import "ufal-theme.css"`, but the co-located ufal-theme.css matched the repo .gitignore `*.css`
rule and was never committed (local builds passed since the file is in the working tree). dtq-dev
force-adds the same file. `git add -f` it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix FE e2e regressions: don't override vanilla License section + navbar a11y

The DSpace Cypress e2e suite (Run e2e tests) had 3 failing specs from CLARIN changes:
- submission.cy.ts + my-dspace.cy.ts: `input#granted` not found. My sections-decorator mapped
  SectionsType.License -> SubmissionSectionClarinLicenseDistributionComponent (ng-toggle variant),
  replacing the vanilla license checkbox. Revert to the vanilla SubmissionSectionLicenseComponent;
  the CLARIN distribution variant stays ported but unwired (activating it needs dtq-dev's matching
  e2e changes too) — documented in sections-decorator.
- header.cy.ts: axe `link-name` (serious) violation — the hidden #repository_path helper anchor (read
  by DiscoJuice) had no discernible text. Added aria-hidden="true" + tabindex="-1".
Validated: npm run build SUCCESS (0 errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port CLARIN file-preview FE cluster (I2 frontend) to v9 standalone

Completes the file-preview feature end-to-end (the BE now serves it via MetadataBitstreamController):
- item-page/clarin-files-section (entry, [item]+[itemHandle]) -> simple/field-components/preview-section
  -> file-description -> file-tree-view (recursive). All migrated to v9 standalone components
  (imports[] + @if/@for control-flow) and lint-clean.
- core/registry/registry.service.getMetadataBitstream(handle, fileGrpType) delegating to the existing
  MetadataBitstreamDataService (data layer was already present).
- Wired <ds-clarin-files-section> into simple + full item-page templates, and added the component to
  both base item-page components' imports[] AND the themes/custom overrides' imports[] (theme overrides
  reuse the base templates, so they need it too or NG8001).

Fixed clarin-license-info (was an orphan never built, so its latent v9 breakages only surfaced now that
clarin-files-section imports it): added NgClass + NgbTooltipModule to imports[]; LocaleService
.getCurrentLanguageCode() now returns Observable<string> in v9 (was string) -> subscribe once into a
currentLangCode field and compare that in isCsLocale(). Added FileSizePipe (clarin-files-section),
NgbCollapseModule (file-tree-view).

Verified: npm run build = BUILD_EXIT=0 (bundle generation complete); scoped eslint on all changed
files incl. HTML templates = 0 errors (2 pre-existing warnings). Full ng lint OOMs locally (RAM), CI
runner has more memory.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix FE unit specs broken by the file-preview cluster wiring

Adding ClarinFilesSectionComponent to the item-page components' imports[] pulled RegistryService
(-> Store) into the FullItemPageComponent/ItemPageComponent spec TestBeds, which don't provide Store
(NG0201/NG0200). Add ClarinFilesSectionComponent to each spec's overrideComponent remove.imports list
(same pattern already used for ThemedFullFileSectionComponent, CollectionsComponent, etc.) so the heavy
child isn't instantiated in those component tests; its tag is ignored via NO_ERRORS_SCHEMA.

Also RegistryService gained a MetadataBitstreamDataService constructor dependency (for
getMetadataBitstream) -> add a jasmine spy provider for it in registry.service.spec.ts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix import placement in item-page specs (was inserted inside a multi-line import)

The previous spec fix inserted the ClarinFilesSectionComponent import inside the existing multi-line
@angular/core import, causing a lint parse error (Identifier expected). Move it to its own complete
import statement + sort. No functional change to the remove.imports / mock-provider fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Wire CLARIN distribution-license submission section (S6)

Override SectionsType.License with SubmissionSectionClarinLicenseDistributionComponent (the CLARIN
distribution-license variant: ng-toggle acceptance + distribution-license-agreement link + help-desk
contact) in sections-decorator.ts, replacing the vanilla checkbox-based SubmissionSectionLicenseComponent.

Update the matching e2e specs to interact with the ng-toggle instead of the vanilla #granted checkbox
(dtq-dev's exact selector): submission.cy.ts + my-dspace.cy.ts now do
`cy.get('ds-clarin-license-distribution ng-toggle').click()`.

npm run build = clean (BUILD_EXIT=0); decorator lints clean; no unit specs reference the section map.
The toggle interaction is verified by the Cypress e2e (runs in CI against a Docker backend).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WIP: CLARIN/LINDAT frontend theme + DiscoJuice login port (Angular 7->20)

Ports the CLARIN/LINDAT visual theme + federated-login onto the vanilla v9 FE (was vanilla before):
- src/aai/ (DiscoJuice aai.js/aai_config.js + discojuice widget JS/CSS/images) + jQuery in index.html
  + script-loader paths (/aai) + angular.json assets/styles wiring.
- CLARIN homepage (ngb-carousel LData/Free-Deposit slides, featured items/stats), header clarin-logo,
  LINDAT/CLARIAH-CZ footer, branding styles (_custom_variables, _clarin-styles), + 3 images.
- clarin-item-box-view: NgClass + NgbTooltipModule imports (v9 standalone).
- i18n repository.title.prefix -> "LINDAT/CLARIAH-CZ Repository".
v9 adaptations: standalone imports, RSSComponent (not RssComponent), firstValueFrom, NgbCarouselModule,
ThemedSearchFormComponent selector, PaginatedSearchOptions. Dev build green.
STILL WIP: visual polish vs dev-5; DiscoJuice localauth needs v9 /login navigation; not runtime-complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Local-run fixes: title separator, CLARIN search box view, home CSR + HAL root retry, DiscoJuice login (namespace+signon gate), SSR disabled for CLARIN-port

- head-tag.service: LINDAT 'prefix + " " + title' (title = 'LINDAT/CLARIAH-CZ Repository Home')
- object-list: render search results via ds-clarin-item-box-view (showClarinViewBox); themed override too
- home-page: browser-only stats loads; resolver server-skip + catchError
- hal-endpoint: retry transient root-endpoint failure (SSR bootstrap race)
- server.ts: CSR home route + bake home title into CSR html
- clarin-navbar-top: gate signon link until AAI/DiscoJuice bound (scriptsReady); parallel script load
- aai.js: derive namespace from <base href>
- environment.production: ssr.enabled=false (CLARIN-port SSR crashes; app renders CSR)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Admin sidebar a11y aria-label + CLARIN dso-selector i18n port

- expandable-admin-sidebar-section: aria-label uses semantic accessibilityHandle
  (?? section.id) so it resolves to e.g. 'Toggle New section' instead of a generated id
- en.json5: dso-selector.placeholder = 'Search for a {{ type }}' (CLARIN/LINDAT wording;
  vanilla v9 was 'Search for {{ type }}')

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port missing language-selector flag images (en.png, cs.png) from dtq-dev

The clarin-navbar-top language switch referenced assets/images/en.png and cs.png which were
never ported, so the flags rendered as broken images. Restored from origin/dtq-dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Port LINDAT/CLARIAH-CZ production header (lindat-common) into the dspace theme

Ported from lindat-merge-dtq-dev-2025-03-07 (the branch dev-5/production runs), adapted to v9
standalone components:
- themes/dspace header: CLARIN top bar (flags + DiscoJuice sign-on) + dark lindat-common
  navigation (LINDAT/CLARIAH-CZ logo, Catalog/Repository/Education/Projects/Tools/Services/About,
  DARIAH-EU + CLARIN logos) + the full 773-line lindat-common stylesheet
- themes/dspace header-navbar-wrapper: renders only the header (production parity - the LINDAT
  layout has no separate DSpace navbar row; the dark header carries the whole navigation)
- themes/dspace _global-styles: LINDAT alert/toggle/pagination/menu-item overrides
- LINDAT favicon (svg+ico), header assets (LINDAT-CLARIAH-cz-gray_0.svg, dariah-eu.png, clarin.png)
- cs.json5: navbar.* translations (Czech)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Re-enable SSR + resolve independent-review findings (lint, specs, UX fixes)

SSR is back on for production: the HAL root-endpoint retry (hal-endpoint.service) turned out to fix
the render-poisoning that previously crashed every SSR page, so ssr.enabled=true again, the
temporary CSR carve-out for /home in server.ts is removed, and /home, /login, /register now render
fully server-side (verified locally; LINDAT markup + title in the initial HTML).

Review fixes:
- themes/dspace header: About dropdown no longer navigates away (routerLink removed from the
  toggle); ds-impersonate-navbar restored (renders only while impersonating)
- object-list: only Item results render in the CLARIN box view; other result types (communities,
  collections) fall back to the standard list element instead of rendering blank
- footer: badge/logo images point at the bundled assets instead of hotlinking production
- home-page: control-flow syntax (@for/@if), split multi-import, guard against undefined facet
  search links; hal-endpoint: drop aliased rxjs imports, enrich the no-payload warning
- cs.json5: language.english/language.czech
- specs: head-tag spec prefix mocks match the CLARIN 'prefix + space + title' composition;
  hal-endpoint spec expects the retry delay; cypress specs adapted to the LINDAT UI (homepage
  title/news, header without vanilla lang-switch/login-menu/search box, statistics navigate
  directly, search results in the CLARIN box view) following the reference branch's pattern

Validated: npm run build:prod clean; targeted eslint clean; karma head-tag+hal-endpoint 37/37 and
object-list 10/10; madge no circular deps; LINDAT Playwright suite 17 passed / 0 failed / 20
skipped (env-gated) against the local Docker stack with SSR enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix CI e2e failures: axe landmarks/list/link-name violations + spec adaptations

Accessibility (axe) fixes in the LINDAT markup:
- themed header / footer: the nested lindat-common <header>/<footer> elements registered duplicate
  banner/contentinfo landmarks - changed the inner elements to <div> (visually identical)
- footer: <br/> separators inside <ul> violated the axe 'list' rule - replaced with a styled
  spacer <li aria-hidden="true">
- object-list: the CLARIN box view now renders inside an <li> (a <ul> may only contain <li>)
- clarin-item-box-view / clarin-license-info: anchors with potentially-empty text (owning
  community, publisher, license name) render only when the text exists (axe link-name)

Cypress spec fixes after the first CI run:
- collection/community/item-statistics: navigate directly to the object's own statistics page
  (where the removed navbar link led), not the site-wide /statistics
- my-dspace: [...$items] -> $items.toArray() - the page now ships jQuery 2.1.4 (DiscoJuice
  dependency, same as the v7 reference) which lacks Symbol.iterator

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* CI: Verify-SSR homepage check accepts the LINDAT repository title

The step grepped the SSR-rendered <meta name="title"> for the literal 'DSpace', but this branch
brands the repository as 'LINDAT/CLARIAH-CZ Repository'. Accept either (SSR itself is what the
step verifies). Everything else in the job was already green (lint, unit, build, all cypress e2e).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Production visual parity: Bootstrap 4->5 class migration + layout fixes

Compared against the running LINDAT production UI and fixed the systematic differences:
- Bootstrap 4 -> 5 utility-class migration across all v7-ported templates (32 files): pl-/pr-/ml-/mr-
  -> ps-/pe-/ms-/me-, float-left/right -> float-start/end, font-weight-* -> fw-*, badge-{color} ->
  text-bg-{color}, pull-left -> fa-pull-left. These classes silently did nothing under Bootstrap 5,
  which broke paddings everywhere and left the sign-on badge on the left (ml-auto) instead of the
  right as in production.
- Bootstrap 5 gives direct .row children width:100%, which stacked the home 'Advanced Search |
  Communities & Collections' line vertically and stretched the item-box type/files/license labels
  to full width - those inline-label rows are now d-flex (matches the v7/BS4 render).
- footer/header: the nested lindat-common <footer>/<header> elements are back (the lindat-common
  stylesheet targets them by element - the previous <div> swap collapsed the footer columns) with
  role="presentation" so they register no duplicate landmark (axe); the footer <br/> group
  separators inside <ul> are a styled spacer <li> with the lindat dash marker suppressed.

With the accompanying backend change (exposed dspace.ui.url + homepage discovery configuration),
the home page now renders the author/subject/language quick links and the item boxes show the
owning community, authors and license labels - matching production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Production parity round 2: CLARIN item page, search facet labels, comcol landing, login logo

Systematic page-by-page comparison against the production LINDAT UI (7 page types, multi-agent
DOM diffing) and fixes for every user-visible difference:
- item view: the CLARIN untyped-item layout is back (citation ref-box with BIBTEX/CMDI + copy,
  share row, icon-labelled metadata fields incl. sponsor/acknowledgement, subject chips,
  collections + 'Show full item record' link); tooltip-module imports fixed in components the
  template pulls in (clarin-ref-citation, clarin-identifier-item-field, subscription button,
  item-move); themed custom-theme override mirrors the base imports
- full item record: citation ref-box, URL auto-linking + complex-value separator in metadata
  values (makeLinks + dsReplace), language column only for admins, CLARIN container frame,
  duplicate vanilla file section removed
- i18n values synced with the fork (Authors, Date issued, Item identifier, Subject(s),
  Show full/simple item record, 'x out of y results', Has Files, 'Limit your search')
- community/collection landing matches production: communities open the subcommunity/collection
  lists (defaultBrowseTab: comcols), the embedded comcol search hides the filter sidebar;
  browse label back to small (h5)
- search: view-mode switch defaults to list-only (hides itself, as in production); the cypress
  grid-view test is disabled with a note
- login page: CLARIN logo next to the repository logo
- community/collection models: handle falls back to the REST handle field when dc.identifier.uri
  metadata is absent (CLARIN 7.x-era data)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Restore v7 loose-null template semantics lost in the @if migration; adapt item specs to the CLARIN layout

The *ngIf -> @if conversion turned the fork's loose null checks (x == null / x != null) into strict
ones. For unbound/optional values that are undefined (not null) the branches then flipped: the item
page Size row rendered empty (clarin-generic-item-field renders values only when type == null),
withdrawn items would show the 'replaced' tombstone, the navbar/license/handle/autoregistration/
item-box templates picked the wrong branch. Restored loose equality in 12 templates.

Unit specs: untyped-item and full-item-page specs now mirror the CLARIN templates - the themed
override removes the CLARIN child components (ClarinRefBox pulls HardRedirectService,
ConfigurationDataService -> Store) and asserts the citation box, the generic metadata rows and the
clarin collections field instead of the removed vanilla field components. 55/55 locally; full suite
5597 passed with only the two environment-dependent LocaleService specs failing locally (pass on CI).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make the undefined-safe template checks lint-clean (template/eqeqeq, no-negated-async)

The previous commit restored loose null checks, but @angular-eslint/template/eqeqeq forbids
==/!= in templates. Replace them with equivalents that keep the undefined-safe semantics:
truthiness for strings/objects that are undefined-or-null when absent, explicit
=== null || === undefined for the numeric handle resourceTypeID (0 is a valid type id), and
((observable | async) ?? null) === null where no-negated-async forbids negating the async pipe.

ng lint: 0 errors. untyped-item + full-item + item-page specs: 66/66 locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Pixel-parity round: restore the v7 LINDAT layout metrics and page structure

An independent designer-critic review of 9 page pairs against the production UI
(lindat.mff.cuni.cz) found layout drift from the Bootstrap 4->5 and *ngIf->@if migrations.
Fixes, page by page:

- global: cap the xxl container at 1140px (BS4 had no xxl tier), BS4 grid gutters (30px),
  input border #ced4da, BS4 list-group/table-cell/card/alert/breadcrumb metrics, card border
  rgba(0,0,0,.125)
- item + full item: the CLARIN white card frame (silver borders + shadow) ported to the item
  pages' own SCSS (it only existed inside the home page's :host scope), .clarin-item-page-field
  row padding + dotted separators, italic metadata links (WCAG rules from customer/lindat),
  full-item element order (collections before the license box and files), plain left-aligned
  'Show simple item record' links top and bottom, ref-box gutter padding neutralized
- item boxes (home + search cards): corner badges pulled flush with the card border again
  (BS4 rows did this via negative margins), body content at the v7 inset, type icon anchored
  to its own column (BS5 dropped position:relative from columns)
- search: view-mode switch hidden (base default, as the v7 fork), no funnel icon on the scope
  button, the empty configuration switcher no longer pushes the sidebar down 32px (collapsed
  margin through a zero-height element), RSS only on home + comcol pages (v7 whitelist)
- community/collection: v7 tab sets (collection lands on 'Recent Submissions' at the root,
  community has no Search tab), landing renders the standard list elements at 20 per page
  without a search form or 'Search Results' heading, sub-list toolbars show the RSS button,
  h2 comcol list headings, 'Collections in this Community' casing, canonical
  hdl.handle.net URL for bare comcol handles, breadcrumb trail kept visible on leaf routes
  without own crumbs (BreadcrumbsService hides the whole bar otherwise)
- browse: the starts-with input group constrained to its column again (BS5 columns lost
  max-width so the input-group's width:100% won)
- home: 48px search input restored (the CSS targeted only the theme-override selector that is
  not in the DOM), the color stripe bleeds edge to edge again
- login: CLARIN logo sized and spaced like v7

Verified: independent critic re-review (community=match, home/search/browse/community-list=
minor), ng lint 0 errors, karma on affected specs green, Playwright 17/17 passed three times.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Adapt the my-dspace e2e spec to the hidden view-mode switch

The view-mode switch is hidden everywhere (list-only results, as on production), so the
detailed-view button no longer exists - the same adaptation the v7 fork made to this spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Final parity polish: production paddings, 20-per-page comcol landing, login form width

Measured directly on production (lindat.mff.cuni.cz) at a 1600px viewport:
- containers have 1rem side padding and the breadcrumb a full spacer (Bootstrap 5 computes 12px
  for both) - this was the shared root cause of the 4px offsets the critics kept measuring on
  the breadcrumb trail, the Statistics button and the license-box centering
- the comcol landing pages list 20 items per page: the previous field-initializer override was
  ineffective because the parent constructor captures defaultPagination before subclass field
  initializers run - the override now happens in the constructor followed by re-initialization
- the login form controls are 280px wide (the shared .login-container computes 302px)

Verified live against production values: breadcrumb/container 16px/16px, collection landing
20 rows, login input 280px. Playwright 17/17, ng lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: keep the query box on explicit comcol /search routes, null-guard full-item values

Independent code review of the parity commits found two fixable defects:
- searchEnabled=false applied to BOTH comcol sub-routes, so a deep link to
  /communities|collections/:id/search lost its query box; the flag now comes from route data
  (landing false, explicit /search true)
- the full-item metadata table called mdValue.value.split() unguarded - one null-valued
  metadata entry would break the whole /full page render

Two other findings are intentional v7-fork behavior (documented in the progress file): the
view-mode switch stays hidden everywhere, and the workflow 'view full item' page uses the
handle-based CLARIN files section like the v7 fork does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Port the CLARIN admin sidebar entries (handle table, ePIC handles, license administration)

Closes the documented C3 review item: the routes existed but were unreachable through the UI.
The v7 fork's menu.resolver.ts entries are ported as a v9 menu provider (site-admin only,
same icons and i18n keys). Verified live: all three entries render in the admin sidebar and
navigate to the existing pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Restore CLARIN static pages, admin GUI fixes, ISO facet, license tab, Czech i18n

- C4: port src/static-files (82 html incl. cs/ subtree, British+American
  licence spellings byte-for-byte) + angular.json assets entries; exclude
  static-files from eslint (content assets, not Angular templates).
  Verified live: raw /static-files/about.html 200, rendered
  /static/about.html 200 with content, cs/faq.html 200, both licence
  spellings 200.
- M2/C1: register Handle + MetadataBitstream in provide-core models[] as
  value imports (v9 getClassForType deserialization).
- M1: providers:[NgbActiveModal] on ClarinLicenseTableComponent (NG0201).
- M4: FilterType.isoLanguage + filterTypeMap mapping to
  SearchTextFilterComponent (decorator alone is tree-shaken).
- M6 FE half: FeatureID.CanManageLicense, functional
  itemPageLicenseMapperGuard (dsoPageSingleFeatureGuard, mirrors v9
  collection-mapper guard), license child route with title data + guard,
  en+cs i18n title/head keys.
- FE-I18N-CS-1: merge 597 fork-added Czech keys missing at HEAD into
  cs.json5 (dtq values, alphabetical placement; vanilla-drift keys
  excluded; 5 pre-existing duplicate keys untouched).

Lint: ng lint 0 errors (warnings unchanged from baseline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Restore CLARIN share-submission button, FE CI workflows, karma specs (C4/M5)

- M5 FE-SHARE-1: shareSubmission() on workspaceitem actions (GET
  /submission/share?workspaceitemid= via HALEndpointService+RemoteDataBuild,
  redirect to /share-submission with changeSubmitterLink; spinner state;
  ShareSubmissionLink interface; v9 @if control-flow template; canEditItem$
  gated). i18n keys already present.
- DEV-CI-FE-1/2: dataquest-dev guards + dataquest/dspace-angular image names,
  reusable-docker-build ref repointed to
  dataquest-dev/DSpace@ufal/clarin-dspace-upgrade-v9, dtq operational
  workflows (deploy/import-weekly/erase_db/create_bitstreams/playwright/
  trigger-ui-tests/tag-release/trigger-builds/new_issue_*) + composite
  erase-db/import-db actions + copilot-instructions. Schedules only fire from
  the default branch; run-on jobs are self-hosted-runner env-gated.
- TEST-FE-KARMA (partial): port html-content.service.spec +
  static-page.component.spec from dtq (C4 spec oracles).

Lint: changed files 0 errors. build:prod green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Lint-fix ported karma specs (import sort, trailing commas)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* CI: gate docker-deploy to manual dispatch; withdraw unadapted karma specs

The dtq html-content/static-page specs fail against the v9 implementations
(locale-fallback + contentState internals differ) - they need real
adaptation, tracked under TEST-FE-KARMA, not a byte port. The C4 static-page
feature itself is live-verified (raw + rendered + cs pages 200).
docker-deploy needs the self-hosted deploy infra (env-gated); run manually.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: provide HALEndpointService + RemoteDataBuildService in WorkspaceitemActions spec

The shareSubmission port added two constructor injections that the spec's
TestBed did not provide, failing all 8 tests in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* port: CLARIN item-type icons + error.png fallback from dtq-dev

clarin-item-box-view resolves ./assets/images/item-types/{dc.type}.png and
falls back to error.png onerror; none of these shipped in the v9 port, so
home/search pages logged 404s and showed broken type icons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: port 4 CLARIN specs from dtq-dev, adapted to Angular 19 standalone

clarin-name-field-parser, share-submission-page, ds-dynamic-autocomplete,
ds-dynamic-sponsor-autocomplete (+ their lookup-relation/metadata-value mocks).
Adaptations: standalone components move from declarations to TestBed imports;
DynamicFormsNGBootstrapUIModule no longer exists in @4science ui-ng-bootstrap 19.
17/17 green locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* lint: eslint --fix on the 4 ported specs + mocks

The spec-port commit passed karma locally but failed the CI lint job
(import order, comma-dangle, indent). ng lint --fix; 17/17 karma re-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* restore vanilla community search: Search tab + default landing + facet sidebar

The LINDAT-parity pass removed the community Search tab, made subcoms-cols the
landing and disabled the search-section sidebar. Per review, communities go back
to full vanilla 9.3 behavior: Search tab (first, default landing) and
community.searchSection.showSidebar=true. Collections keep the LINDAT
'Recent Submissions' landing without sidebar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Milan Majchrak <minptai7@gmail.com>
The Docker images workflow on this branch is still vanilla DSpace's: it only
triggers on pushes to 'main' / 'dspace-**', so nothing was ever built for
dtq-dev-9-base and no images reached hub.docker.com/u/dataquest.

Trigger the workflow on pushes to dtq-dev-9-base (plus PRs and manual runs) and
pin the reusable workflow to dataquest-dev/DSpace@dtq-dev instead of
@ufal/clarin-dspace-upgrade-v9. The vanilla v9 reusable builds through
'ghcr.io/${image_name}' -> 'ghcr.io/dataquest/*', a namespace the dataquest-dev
org does not own, so it could never push. @dtq-dev pushes straight to DockerHub
and is what builds our customer/* images today.

Swap the tag flavours to match dtq-dev: the production ("dist") build takes the
plain tag and the development build is suffixed '-dev'. Previously the dev build
took the plain tag, so 'dataquest/dspace-angular:dtq-dev-9-base' would have been
a non-SSR dev image rather than the deployable production one.

Port scripts/sourceversion.py from dtq-dev (missing on this branch) and enable
run_python_version_script, so /VERSION_D.html reports the built commit as it
does on dtq-dev.

Also drop the vanilla 'docker-deploy' smoke-test job: it consumes
docker-image-*-linux-amd64 artifacts that the dtq-dev reusable does not upload,
and it is gated on workflow_dispatch, which this workflow now enables.

Images published as tag 'dtq-dev-9-base':
  dataquest/dspace-angular (dist/prod), dataquest/dspace-angular:-dev

Co-authored-by: Milan Majchrák <minptai7@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3 on dev-6 (#1383)

* ci(deploy): deploy + vanilla import for instance 8603 on dev-6

deploy.yml on this branch was still the stale dev-5 one (instances 5/8,
runs-on: dspace-dep-1) and referenced build-scripts/run/start.sh, which does not
exist on this branch -- so it could not have run at all.

Retarget it at instance 8603 on dev-6, modelled on customer/jcu (the closest
analogue: also DSpace 9.3, also dev-6, and it already carries the vanilla-import
flow). Port build-scripts/run/start.sh from customer/jcu.

The import job wipes the DB, loads the SQL dump and then runs the vanilla
migration, as on customer/*. The dump path is an input (DUMP_PATH) rather than
jcu's fixed <instance>/dump/old_dump.sql convention, because this dump sits at
/opt/dspace-envs/8603/dspace_dev5_dump_26.07.16.sql.

The migration deliberately runs as a one-off `docker compose run` rather than
`docker exec`; see the comment in the step for why (entrypoint auto-migrates
without `ignored`, webapp crashes on the un-migrated dump, restart policy
SIGKILLs the exec with 137). Kept verbatim from customer/jcu.

A probe step reports the target environment (config dir, .env keys with values
redacted, containers, postgres port, dump file) before anything is touched. It
is failure-tolerant and never fails the deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): split probe into its own job + PROBE_ONLY; fix dump path

First run on dev-6 (29501166150) proved two assumptions wrong:
- the dump is NOT at /opt/dspace-envs/8603/<file>.sql; that dir holds a dump/
  subdir, so DUMP_PATH now defaults to 8603/dump/dspace_dev5_dump_26.07.16.sql
  (jcu's convention after all)
- postgres inside dspacedb8603 listens on the DEFAULT 5432, so copying jcu
  (no -p flag) is right; mendelu's -p 10573 is instance-specific

The deploy then failed with 'Bind for 0.0.0.0:5432 failed: port is already
allocated', so make the probe a standalone job with PROBE_ONLY, to diagnose
port ownership without deploying anything.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): stop inheriting vanilla's fixed host ports (5432/8080/8983/4000)

Root cause of 'Bind for 0.0.0.0:5432 failed: port is already allocated' on 8603,
proven by 'docker compose config' on dev-6:

  dspacedb (container_name: dspacedb8603)
    ports:
      target: 5432, published: "5432"    <- vanilla docker-compose-rest.yml
      target: 5432, published: "10603"   <- /opt/dspace-envs/8603 overlay

Compose MERGES ports lists across -f files instead of replacing them, so the
per-instance overlay adds 10603 but still inherits vanilla's fixed 5432. Those
fixed ports are global to the dev host, so only one instance can own them --
dev-6 currently has customer/jcu holding 0.0.0.0:5432, :8080 and :8983, and
customer/mendelu holding :4000, so 8603 could never bind them.

Add docker/docker-compose-instance-ports.yml, which resets ports to [] between
the vanilla files and the instance overlay; the overlay then supplies the real
instance-scoped ports (10603/6603/11603/4603 for 8603). This keeps the fix in
git rather than requiring /opt/dspace-envs/8603/docker-compose.yml to be patched
on the machine -- mendelu and sav solve the same problem there with
'ports: !override', which is why they publish only 127.0.0.1:105xx.

start.sh took the same treatment: it ran compose with the vanilla files ONLY, so
its 'up' collided too. It now honours INSTANCE_OVERLAY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): flyway repair before migrate; fix workflow_call dump default

Import loaded the 37MB dev-5 dump fine, then the migration died:
  FlywayValidateException: Detected applied migration not resolved locally:
  7.6.2026.06.01.
The dump is from a CLARIN 7.6 instance, so flyway_schema_history lists
CLARIN-specific migrations that this vanilla v9 tree does not carry, and Flyway
refuses to migrate at all. Run 'dspace database repair' first to mark those
entries deleted, then 'migrate ignored' as before.

Also: the workflow_call DUMP_PATH default still pointed at the pre-probe path
(8603/<file>.sql instead of 8603/dump/<file>.sql) -- only the workflow_dispatch
default had been corrected, so import-weekly.yml would have used a path that
does not exist.

import-weekly.yml passed INSTANCE '8', which no longer matches any job in
deploy.yml, so the scheduled run would have skipped everything and still gone
green. Point it at 8603 and leave the cron disabled: it carries ERASE_DB: true
and an unattended weekly wipe of 8603 was never asked for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): probe reports data + service health after an import

A green import job only proves the commands exited 0. Report the flyway schema
version actually reached, row counts for item/bitstream/collection/community/
eperson, dspace version and whether the REST API answers, so an import that
'succeeded' onto an empty or half-loaded DB is visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): probe reads schema_version, not flyway_schema_history

DSpace overrides Flyway's history table name -- DatabaseUtils.FLYWAY_TABLE is
"schema_version", kept for backwards compatibility. The probe queried Flyway's
default name and reported 'relation "flyway_schema_history" does not exist' on
a database that had in fact migrated fine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): probe shows what database repair did to the CLARIN history

Report the 7.6/DELETE rows in schema_version and the surviving CLARIN tables, so
that 'repair only rewrites bookkeeping, it does not drop data' is something the
log demonstrates rather than something the docs claim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* TEMP: check dump for pre-existing DELETE marker

* TEMP: refine dump grep

* ci(deploy): drop temporary dump-inspection step

It answered its question: the dump carries exactly one schema_version row for
7.6.2026.06.01 (installed_rank 80, type=SQL) and no DELETE marker, so the
type=DELETE row at rank 81 in the live DB is one that 'database repair' added --
not something inherited from dev-5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): drop docker-compose-instance-ports.yml, rely on the overlay

The per-instance overlays in /opt/dspace-envs/<instance>/ now use 'ports: !override'
themselves, which is where this belongs -- same as customer/mendelu and customer/sav.
That makes the repo-side reset file redundant, so remove it and every reference.

start.sh still honours INSTANCE_OVERLAY: without the overlay in its compose chain it
would bring the stack up on the vanilla fixed ports and there would be no !override to
replace them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(deploy): probe says outright whether the overlay overrides the fixed ports

With the reset file gone, both compose invocations were identical, so the second
claimed to show 'without the instance overlay' while still passing it (and left a
stray line-continuation behind). Compare against the vanilla files alone, and add an
explicit verdict: if a fixed 5432/8080/8983/4000/9876 survives the merge, the overlay
is missing 'ports: !override' and the deploy will fail to bind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: weekly redeploy instead of weekly import for 8603

8603 is not meant to be re-imported on a schedule, so drop import-weekly.yml
rather than keep it pointed at this instance. It also carried ERASE_DB: true,
i.e. an unattended weekly wipe.

Replace it with redeploy-weekly.yml, which calls the same deploy.yml with IMPORT
and ERASE_DB both false. That is non-destructive: start.sh pulls first, so the
instance picks up the latest dtq-dev-9-base images every Sunday while the
database and assetstore are left alone. Loading a dump stays a manual 'Deploy
DSpace' run with IMPORT checked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Milan Majchrák <minptai7@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Finish pipeline for deploy docker and playwright

* Added forgotten variables

---------

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Serve the Spring Boot backend under a configurable context-path so an
instance can run behind a /repository/server namespace without a Java
code change or image rebuild, plus the FE/CI follow-ups that path implies.

* docker/docker-compose-rest.yml: add a parameterized
  server.servlet.context-path knob (SERVER_SERVLET_CONTEXT_PATH, default
  /server). DSpace registers this property source before the embedded
  Tomcat starts, so it overrides the built-in /server default. Set
  per-instance via the .env passed to `docker compose --env-file`
  (e.g. SERVER_SERVLET_CONTEXT_PATH=/repository/server).
* server.ts: guard the sitemap/signposting proxy pathRewrite against a
  leading double slash when ui.nameSpace is non-root (e.g. /repository);
  no-op for the default nameSpace: /.
* src/app/core/data/epic-handle-data.service.ts: build the fallback self
  link from the resolved REST endpoint instead of a hardcoded /server/api
  path, so it is namespace-aware.
* .github/workflows/deploy.yml: point the instance REST smoke probe at
  /repository/server/api.

Config/FE-only: no Java changes, no image rebuild. Removing the nginx
path-rewrite on the deployment and patching the JWT external-login cookie
path (Shibboleth/OIDC/ORCID/SAML) are tracked as separate follow-ups.

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the upstream DSpace pull-request template with the dataquest
customized version already in use on dtq-dev (Problem description /
Analysis / Problems / Sync verification / Manual Testing / Copilot
review), so dtq-dev-9-base uses the same PR template.

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…9 base (#1389)

* Make Solr heap tunable via SOLR_HEAP in docker-compose-rest.yml (default 4g)

Manual re-apply (ADAPT) of dtq-dev commit d8c5d54: the fork hunk edited the 7.6
compose entrypoint line ('exec solr -p 8983 -f -m 4g') which no longer exists on the
vanilla 9_x entrypoint ('runuser -u solr -- solr-foreground'). Applied as an
environment block entry SOLR_HEAP: "${SOLR_HEAP:-4g}" on the dspacesolr service -
the official solr:9-based dspace-solr image's solr-foreground honors SOLR_HEAP and
runuser without -l preserves the environment. Quotes kept per the fork squash
('Add Solr params into quotes'). Per-instance override works via deploy.yml's
--env-file /opt/dspace-envs/$INSTANCE.

Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md par.5 / d8c5d54 (FE-1, Vlna 1).

* Pass PASSWORD_ADMIN secret to Playwright UI tests (remainder of #1372 port)

Port remainder (ADAPT) of dtq-dev commit af0c70f: the functional core of that
commit - removing the broken 'Copy customer config' step referencing the absent
config/config.lindat.ui.tests.json - was already applied on dtq-dev-9-base by the
newer v9-side commit c22bfa0 (Lindat9/Update pipeline in GA, #1386), which also
retargeted HOME_URL to dev-6.pc:8603 and set NAME: LINDAT-9 (the lindat-9 profile
exists in dspace-ui-tests/customer-constants/customers/). Those newer v9-side
choices are deliberately kept: NAME stays LINDAT-9, NOT the fork's DEFAULT.
The only fork delta still missing was PASSWORD_ADMIN: dspace-ui-tests/scripts/test.sh
forwards PASSWORD_ADMIN into the test container (login-dependent specs), so this
commit adds it from the existing DSPACE_ADMIN_PASSWORD repo secret.

Deviation from the sync plan card af0c70f recorded: AC2 (byte parity with fork
head playwright-tests.yml) is obsolete due to target-branch drift (#1386-#1388);
disposition changes PORT -> ADAPT/partially-superseded.

Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md par.5 / af0c70f (FE-1, Vlna 1),
with documented AC deviations.
…sable + pre-delete warnings (#1335, #1357, #1373) (#1395)

ADAPT port of the dtq-dev user-deletion cluster, sourced from origin/dtq-dev HEAD
file content (NOT sequential picks): 07957d8 (#1335 parent feature) +
43cf493 (#1357 isCurrentUser || isSelfDeletionError notification fallback) +
96209c4 (#1373 tooltip container="body" clipping fix).

v9 adaptations:
- eperson-delete-guard.service.ts: @Injectable({ providedIn: 'root' }) instead of
  registration in access-control.module.ts (file does not exist on v9 - standalone);
  imports rewritten to v9 eslint style.
- epeople-registry + eperson-form: *ngIf/ng-template markup rewritten to @if/@else;
  NgbTooltipModule + BtnDisabledDirective added to standalone imports[]; v9 modal
  'name' binding kept (NOT the fork's dso/DSONameService binding); error branch uses
  the fork's '.failure' key (vanilla wrongly reuses '.success'); fork's
  observableOf(buildPaginatedList(...)) empty-list tweak carried consciously.
- confirmation-modal: optional warningLabel input rendered via
  {{ warningLabel | translate:{ dsoName: name } }} - bound to v9's 'name' input,
  optional so other modal consumers render unchanged.
- i18n: 4 new keys (notification.deleted.forbidden.self + 3x delete.warning.*)
  hand-merged into CLARIN-modified en.json5 + cs.json5 (cs with commented-English
  convention).
- Specs: eperson-delete-guard.service.spec.ts ported; registry/form/modal specs
  extended with the cluster's tests converted to v9 standalone TestBed style
  (of instead of observableOf, RouterMock, root-provided guard service).

Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md §5 / 07957d8 + 43cf493 +
96209c4 (FE-2, Vlna 2).
The `subjectFirstValue` hierarchical facet (used e.g. in the homepage
search configuration) had no i18n keys, so the facet header, placeholder
and search label rendered as raw keys
(`search.filters.filter.subjectFirstValue.head`, ...).

Add the missing `.head`, `.placeholder` and `.label` keys for
`subjectFirstValue` in en.json5 and cs.json5, mirroring the existing
`subject` facet strings.

Fixes dataquest-dev/dspace-customers#835

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1397)

* Clarin9/Restore DiscoJuice auto-popup on login page (#834)

The DSpace 9 upgrade (#1316) re-based LogInPasswordComponent onto the
upstream component and dropped the CLARIN/LINDAT DiscoJuice auto-popup
logic, so the federated-login popup no longer opened on /login.

Restore that logic:
- re-add SHOW_DISCOJUICE_POPUP cookie handling (initializeDiscoJuiceCache,
  toggleDiscojuiceLogin, popUpDiscoJuiceLogin) which clicks the navbar
  sign-on link DiscoJuice binds to
- inject CookieService + PLATFORM_ID and guard the DOM/cookie work with
  isPlatformBrowser (SSR-safe under Angular's newer server renderer)
- provide CookieService in the component spec

DiscoJuice scripts, CSS and assets were already present; only the
trigger was missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update docs

* Fix spec: restore accidentally removed HardRedirectService import

The previous commit's spec edit replaced the HardRedirectService import
instead of adding CookieService alongside it, breaking test compilation
(TS2304). Re-add it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Provide CookieService in log-in + container specs

LogInPasswordComponent now depends on CookieService, so specs that
render it transitively (LogInComponent, LogInContainerComponent) must
provide it too. Add CookieServiceMock providers to both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* e2e: close DiscoJuice popup in loginViaForm before typing

Restoring the DiscoJuice auto-popup means it now covers the local login
form on /login, so cy.type() into the email field times out across all
form-login e2e specs. Mirror the dtq-dev helper: close the popup
(.discojuice_close) before filling the credentials.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Open DiscoJuice popup reliably (poll until bound) instead of fixed delay

The fixed 250ms click raced against async AAI script loading: when it
fired before DiscoJuice bound, the popup never opened (CI showed
div.discojuice staying display:none). Poll (bounded, outside the Angular
zone so it never stalls SSR / whenStable) until DiscoJuice has created
its popup markup, then click. Cancel the timer on destroy.

Also make the e2e loginViaForm wait for the popup to be visible before
closing it, instead of asserting mere existence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address PR review: test DiscoJuice popup logic + assert password field

- log-in-password.component.spec.ts: add unit tests for the SHOW_DISCOJUICE_POPUP
  cookie logic — first visit seeds the cookie to true, cookie=true triggers
  popUpDiscoJuiceLogin() (kept true), cookie=false suppresses once then resets to
  true. Trigger asserted via a spy so tests stay deterministic (no timer/DOM).
- cypress/support/commands.ts: loginViaForm() now asserts the password field is
  visible before typing, matching the email field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Clarify popUpDiscoJuiceLogin comment

Rewrite the doc comment to accurately describe the timer-based approach and
drop the incorrect "stall SSR rendering" note (the popup logic is guarded by
isPlatformBrowser and never runs on the server). No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e href (#876) (#1428)

* Clarin9/Fix "latest version" notice link ignoring the base href (#876)

The `item.version.notice` translation embeds a raw `<a href='{{destination}}'>`
anchor which the alert renders through `[innerHTML]`, so the interpolated value
is resolved by the browser, not by the Angular router. `getItemPage()` returned
the bare router path `/items/<uuid>`, and `<base href>` does not apply to
root-relative URLs, so on a sub-path deployment (`<base href="/repository/">`)
the link pointed at `/items/<uuid>` and 404'd.

Resolve the route through `Location.prepareExternalUrl()` - the same transform
`RouterLink` applies to its own `href` - which is a no-op when the base href
is `/`, so vanilla deployments are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Review feedback: cover the vanilla base href and tighten the spy assertions

- getItemPage now declares the contract it always had: the template calls it while
  the latest version is still loading, so item and the returned url may be
  undefined (Copilot).
- Reset the shared Location spy before acting, so the "the plain router path is
  what gets handed to Location" assertion cannot be satisfied by the call the
  template already made during initial rendering, and assert the hasValue guard
  really short-circuits (Copilot).
- Add a describe that drops the Location stub and exercises the real
  PathLocationStrategy against APP_BASE_HREF '/', '/repository/' and '/repository'
  (the form express passes on the server), plus an entity-typed item. Without it
  the "no-op when the base href is /" claim - the case every vanilla install runs
  - lived only in a code comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Shorten the item page url comments

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… object (#876) (#1429)

* Clarin9/Redirect to login when an identifier resolves to a restricted object (#876)

`lookupGuard` collapsed every failed lookup into a single boolean, so a 401/403
from `/server/api/pid/find` was indistinguishable from a genuine 404: an
anonymous user opening a restricted item by handle got "No item found for the
identifier ..." and had no way to authenticate.

Branch on `RemoteData.statusCode` and delegate the 401/403 case to the shared
`returnForbiddenUrlTreeOrLoginOnFalse` operator, which returns a UrlTree to the
login page for anonymous users (remembering `state.url` so they come back to the
identifier after logging in) and to the forbidden page for authenticated ones.
Any other failure - 404 included - still activates the route so
ObjectNotFoundComponent keeps rendering.

This makes `/handle/...` and `/id/...` behave like `/items/:id`, which already
gets this through `itemPageResolver` -> `redirectOn4xx`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Review feedback: keep the SSR response honest and harden the guard spec

- Set the 401/403 status on the server response before redirecting. Without it
  SSR would serve the login page with HTTP 200 under the identifier's own URL,
  and server.ts would store that in the bot cache for a day. No-op in the browser.
- Add take(1): returnForbiddenUrlTreeOrLoginOnFalse combines with
  isAuthenticated(), a store selector that never completes, so the guard's
  observable relied on the router's own first() to terminate.
- Reword the fallback comment - it is the catch-all for 404, 501, 5xx and
  status-less failures, not 404 only (Copilot) - and drop the claim that the
  authenticated branch behaves identically to /items/:id (a UrlTree rewrites the
  address bar, redirectOn4xx uses skipLocationChange).
- Spec: use a non-completing isAuthenticated stub, assert the guard emits exactly
  once and completes, cover a failure with no status code and 401-while-
  authenticated, assert the server response status, and add a
  TestBed.runInInjectionContext case so the injected defaults are exercised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make the injection-context spec fail if the guard stops emitting

It asserted inside a subscribe callback with no done(), so it would have passed
silently had the observable never emitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Shorten the lookup guard comments

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Assert the SSR status for the two remaining restricted-lookup cases

401+authenticated and 403+anonymous went through the same setStatus call but
were not covered. Verified: all four assertions fail without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Cover the whole non-401/403 fallback, 501 and 422 included

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…876) (#1431)

* Clarin9/Port per-field type binding (submit.type-bind.field "A=>B") (#876)

Upstream type-bind supports exactly one global controlling field. LINDAT needs
per-field control, expressed as `submit.type-bind.field = dc.type,
dc.language.iso=>edm.type`, and none of that machinery was ported to the v9
branch: `FormFieldModel` had no `typeBindField` (so cerialize dropped the REST
value), the parser always stamped the relation with the global type field, and
`FormBuilderService` kept a single `typeField` string and a single type bind
model. Selecting `edm.type = TEXT` therefore evaluated the relation against the
empty `dc.type` model and the dependent language field kept its `d-none` class.

- `FormFieldModel.typeBindField` is deserialized again.
- `FormBuilderService` keeps a map of controlling fields (default + one entry
  per `A=>B` override, order-independent, duplicate-safe, trimmed) and a map of
  registered controlling models, plus a subject that emits every registration.
  `getTypeBindModel(ref?)` takes an optional ref so all existing call sites and
  mocks keep working.
- `FieldParser.getTypeBindFieldRef()` stamps the relation with the controlling
  model id when `<type-bind field="...">` is declared, and otherwise with the
  field's own metadata name, which is resolved against the map later - the
  property arrives over REST asynchronously.
- `DsDynamicTypeBindRelationService` passes the relation id through, no longer
  dereferences a missing bind model, and attaches to a controlling model that is
  only registered by a later `modelFromConfiguration()` call.

Deliberate deviations from the 7.x implementation (documented in the PR):
`findById` and `row-parser` are left untouched, `typeBindField` is not carried
on control models, and the inverted second clause of the 7.x `getTypeBindModel`
guard is dropped - it is a no-op for the LINDAT config and would otherwise fall
back to `dc_type`, reproducing this very bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Review feedback: fix the subscription hand-off, scope the registry, harden parsing

Behaviour:
- subscribeRelations now returns a single owning Subscription. The caller spreads
  the returned array into its own, so a child created after that snapshot - which
  is exactly what the late-registration listener does - could never be torn down
  and kept mutating hidden/disabled on a destroyed model.
- Always listen for type bind model registrations, not only when nothing was
  attached: until the real controlling model exists the field is temporarily
  attached to the default one, and that case was never re-attached (Copilot).
- The type bind registry is now dropped when the submission changes. Sections of
  one submission still share it (a controlling field may live in another section),
  but a model from the previously opened collection's form can no longer answer
  lookups and defeat the fall-back-to-default behaviour.
- Controlling models are registered again once submit.type-bind.field arrives, so
  an override that exists only in the property - with no <type-bind field="..."> in
  the XML - still resolves if the config lands after the form was parsed.
- A self-referencing type bind is now a console.warn + skip instead of a throw: it
  can be raised from the registration callback, and one misconfigured field should
  not take down the whole submission section.
- Tolerate blank/whitespace/malformed values in the property (a null entry used to
  throw inside the subscribe) and trim typeBindField before building the model id
  (Copilot). getTypeBindModel is typed `| undefined` (Copilot).

Tests: late registration now asserts the relation is really re-evaluated and stops
on unsubscribe; added the attached-to-default case, cross-submission isolation, a
delayed configuration response, malformed values, and a cerialize round-trip
proving typeBindField survives deserialization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot follow-up: bound the parsed-rows cache and fix a misleading comment

- typeBindParsedRows is only filled until submit.type-bind.field has been
  processed, and is dropped once it has. A section form re-parses on every data
  update, so the cache would otherwise keep growing and retain the model graph of
  every re-parse for the lifetime of the submission. Without a config service the
  map can never change, so nothing is cached at all.
- Reword the matchesCondition guard comment: getTypeBindModel falls back to the
  default model, so no model at all means neither the field's controlling model
  nor the default one has been registered yet - not that the controlling field is
  permanently absent from the form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot follow-up: trim typeBindField in getTypeBindModelIds too

isNotEmpty('  ') is true in this codebase, so a padded `<type-bind field=" edm.type ">`
would have registered the controlling model as ' edm_type ' while
FieldParser.getTypeBindFieldRef - which does trim - stamps the relations with
'edm_type'. The lookup would then miss, fall back to dc_type and leave the
dependent field permanently hidden, i.e. reproduce the very bug this PR fixes.

The existing end-to-end spec now uses a padded value, so it fails without the trim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Follow the controlling model when its section is re-parsed

Attachment was deduped by model id, but setTypeBindModel emits on identity: when
the section holding the controlling field is re-parsed (section forms re-parse on
every data update) a NEW instance is registered under the same id, and a bound
field in another section kept listening to the dead one - so the dependent field
stopped reacting, which is the A3 symptom again in a narrower form.

Attachment is now keyed by id but compared by identity, and the stale child
subscription is removed from the owning Subscription and unsubscribed, so nothing
accumulates across re-parses either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot follow-up: do not evaluate a self-bound relation at all

Skipping the self-reference in getRelatedFormModel only stopped it from being
subscribed to; evaluateRelations still ran matchesCondition against the field's
own (empty) value on the initial pass, hid the field, and - with nothing attached
- never re-evaluated it, so a misconfigured <type-bind field="..."> pointing at
its own metadata field made that field permanently unreachable.

subscribeRelations now detects the self-reference up front, warns once and returns
without evaluating or attaching anything, leaving the field exactly as rendered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Distinguish a configured self-reference from a not-yet-parsed controlling model

The self-reference guard used the runtime lookup, which falls back to the default
model. For a field whose own id IS the default model id, a relation whose real
target simply had not been parsed yet therefore looked like a misconfiguration:
subscribeRelations bailed out before wiring the registration listener, so the
field never picked up its controlling model.

Resolve the reference from configuration only (new
FormBuilderService.resolveTypeBindModelId, order-independent) to decide whether
the field really is bound to itself; the transient case now just skips the
initial evaluation - so the field is not hidden for no reason - and still attaches
when the real model is registered.

Also add the spec that actually pins the getTypeBindModelIds trim. The one added
in 79f94f1 did not: the suite's config already contributes 'edm_type' through
its A=>B entry, so the padded id was merely an extra miss. The new case configures
only 'dc.type', making the padded <type-bind field=" edm.type "> the sole source
of the controlling id - verified to fail with the trim removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot follow-up: make the default type bind model id explicit

resolveTypeBindModelId declared `string` but ended in a bare Map.get, and
getTypeBindModel fed the same possibly-undefined value into its fallback lookup.
Both now go through getDefaultTypeBindModelId(), which falls back to the
TYPE_BIND_DEFAULT_MODEL_ID constant that also replaces the four scattered
'dc_type' literals. Also fix a comment still naming the pre-rename
dependsOnItself().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot follow-up: route getTypeField through the guaranteed default too

It was the one remaining place that returned a bare Map.get for a method declared
to return string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot follow-up: honest mock return value and an optional typeBindField

- The shared FormBuilderService mock returned undefined from
  resolveTypeBindModelId while the real one always returns a model id. It now
  mirrors the real implementation for a reference the type field map does not
  remap, so a caller that starts using the value does not silently get undefined.
- typeBindField is genuinely optional in the REST payload and the code already
  treats it that way (`?.trim()` in FieldParser and in getTypeBindModelIds, and a
  spec asserting it stays undefined when the attribute is absent). I argued for
  consistency with the other non-optional @autoserialize members earlier; the
  usage asymmetry is the stronger argument, so it is now declared optional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copilot follow-up: align the mock signature with the optional parameter

resolveTypeBindModelId takes an optional ref, which is what the `??` fallback in
the fake is there for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Shorten the type bind comments

Keep the non-obvious reasoning, drop the prose around it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Shorten the type bind spec comments

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci(import-8603): copy the assetstore during import, as dev-5 does

The dump restores the bitstream rows (internal_id, size_bytes, checksum) but
not the files, so /dspace/assetstore stayed empty on the v9 stack and every
bitstream download returned 500 - including tiny licence files, so it was not
route-specific.

It also broke OAI silently. SpecialItemService.getUploadedMetadata() reads the
authored CMDI from the METADATA bundle, and its catch block returns an empty
Document, so an unreadable file degrades to the generated CMDI profile rather
than failing. That surfaced only as six mismatching OAI records in the REST
test suite and took days to attribute.

dev-5 already does this copy in the import-db composite action; the v9 stack
had no equivalent step. Two deliberate differences from dev-5's version:

- The copy runs before `dspace oai import -c`. Copying the files and even
  restarting the container is not enough - the records keep serving the
  generated substitute until that cache is cleared.
- No `chown -R dspace:dspace`. The v9 runtime stage is FROM eclipse-temurin
  with no USER directive, so there is no `dspace` user (the one in the
  Dockerfile exists only in the build stage) and the webapp runs as root, which
  can both read and write root-owned files. dev-5's command fails outright here
  with "chown: invalid user: 'dspace:dspace'".

The step verifies the result inside the container and fails if the assetstore
is still empty, so a silent recurrence is not possible. ASSETSTORE_PATH can be
set to an empty string to import without bitstream files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci(import-8603): simplify assetstore copy to dtq-dev v7 style

Drop the extra guards (missing-path / basename / still-empty checks) and the
long comment, keeping just the empty-check + docker cp that dtq-dev's import-db
action uses for v7. Also shorten the ASSETSTORE_PATH input description.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Restore "Submissions" entry in admin sidebar

The v7->v9 upgrade migrated the CLARIN admin sidebar entries to the new
menu-provider architecture but dropped the "Submissions" link. Re-add it
as a dedicated SubmissionsMenuProvider (link to /mydspace, upload icon,
always visible), matching the v7 fork behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Address Copilot review: use MYDSPACE_ROUTE constant and add provider spec

- Replace hardcoded '/mydspace' with the shared MYDSPACE_ROUTE constant
  so the submissions link stays consistent with the rest of the app.
- Add submissions.menu.spec.ts (mirrors health.menu.spec.ts) to lock down
  the section model — text key, link target and icon — against regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Removed redundant comments

---------

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ons (#876) (#1465)

* Clarin9/Point LINDAT header menu to the portal site links (#876)

The dspace-theme header (the active LINDAT/CLARIAH-CZ theme) linked the main
menu entries to in-app routes (routerLink="community-list", "education",
"projects", "tools", "en/services", "partners"...). Those resolve under the
/repository base href, so the top navigation no longer pointed at the
LINDAT/CLARIAH-CZ portal as in the v7 production theme, failing:
- homePage: "menu options should have correct target links"

Restore the absolute portal targets (/services/catalog, /#education,
/en/services, /partners, ...) with their Czech equivalents, matching the v7
theme. The "Repository" entry keeps routerLink="home" (it links back into the
app).

Re-add the locale helpers the template needs (getLangCode, getLangCodeIfCzech,
translateSlug). In v9 LocaleService#getCurrentLanguageCode is asynchronous, so
the current language is read synchronously from TranslateService#currentLang
for use in the href bindings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Clarin9/Keep the /repository base path when redirecting legacy bitstream URLs (#876)

legacyBitstreamURLRedirectGuard built the redirect target with
`new URL(getBitstreamDownloadRoute(payload), HardRedirectService.getBaseUrl())`.
getBitstreamDownloadRoute returns a leading-slash path (/bitstreams/<id>/download),
so resolving it against a base URL keeps only the origin and drops the
/repository namespace; getBaseUrl() (environment.ui.baseUrl) could also carry
the internal SSR host. The old-style bitstream URL therefore redirected to a
broken location (e.g. http://.../bitstreams/<id>/download without /repository,
which returns 404), failing:
- itemPage: "old bitstream url should redirect to the download page"

Redirect to a namespace-prefixed, root-relative path instead
(`<nameSpace>/bitstreams/<id>/download`) so the browser/SSR resolves it against
the current origin and the target stays inside the app when it is mounted under
a sub-path. Works for a root deployment too (empty namespace).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Address Copilot review: normalize header UI language to en/cs

getLangCode() returned TranslateService.currentLang verbatim. The deployment
enables many languages (config.yml doesn't override the default list) and the
language can be browser-negotiated, so a non-en/cs UI language (e.g. 'de'/'fr')
produced malformed LINDAT portal links such as '/de/sluzby' (foreign locale +
Czech-only slug from translateSlug).

Normalize getLangCode() to the two languages the portal supports: 'cs' for
Czech, 'en' for everything else. English and Czech behaviour is unchanged (so
the menu-links test and v7 parity hold); other locales now fall back to
well-formed English portal links. Also drops the now-unused environment import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Trim header locale-helper comments to one-liners

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Move header language helpers to the base component (as in dtq-dev)

Match the dtq-dev structure: getLangCode/getLangCodeIfCzech/translateSlug now
live in the base header component (src/app/header/header.component.ts) and the
themed dspace header just extends it, instead of carrying the helpers itself.

getLangCode() again follows the current/system UI language (read synchronously
from TranslateService, falling back to the configured language before one is
set) rather than being pinned to en/cs. This restores the dtq-dev behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Shorten the legacy-bitstream-redirect comment

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#1472)

* Clarin9/Redirect back to originating page after DiscoJuice local login (#876)

On the standalone login page the post-login redirect ignored the `redirectUrl`
query param that the DiscoJuice local-auth flow (src/aai/aai.js) appends as the
absolute page URL, so signing in from e.g. the search page always landed on the
home page. `LogInPasswordComponent.submit()` now reads that query param, reduces
it to an app-relative path (the same format `HardRedirectService.getCurrentRoute()`
produces, which `reloadGuard` already consumes), and uses it as the redirect
target; it prefers a nested `redirectUrl` so login is never the target, and keeps
the previous `setRedirectUrlIfNotSet('/')` fallback when no param is present.

Fixes the dspace-ui-tests LINDAT-013 scenario (loginPage.spec.ts "login from
search page should redirect back to search page"). Mirrors the dtq-dev fix
9dff6af, adapted to the refactored v9 component.

Refs #876

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Clarin9/Trim inline comments in log-in-password redirect fix

Shorten the multiline comments/JSDoc added for the redirect-from-search fix
to one-liners; the fuller rationale now lives in the PR description.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Clarin9/Address Copilot review: harden redirectUrl parsing

- getRedirectUrlFromQueryParams() now returns `string | null` and guards against
  non-string (e.g. repeated `string[]`) query params, so login submission falls
  back to the default redirect instead of throwing.
- Normalize the redirect with a string `.replace(/^https?:\/\/[^/]+/i, '')`
  (v7-style, can't throw) instead of `new URL(...)`.
- Add unit tests for an already-relative redirectUrl and a non-string value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…changes (#1489)

* Clarin9/Save submission immediately when a type/sponsor/author field changes

In the DSpace 9 upgrade, the CLARIN section-form lost the logic that persisted
the submission as soon as a type-bind controlling field (e.g. dc.type) or a
sponsor/author autocomplete value was picked. In v7, selecting such a value in
a dropdown fired an immediate section save (verified live on dev-5: choosing a
resource type triggers a PATCH). In v9 nothing was saved until the whole
section lost focus, so the choice could be lost.

Restore the v7 behaviour in SubmissionSectionFormComponent.onChange: when the
changed field is a type-bind controlling field or the sponsor/author field,
dispatch a section save immediately.

The set of type-bind controlling fields is owned by FormBuilderService in v9
(populated from `submit.type-bind.field`), so expose it via a new
getTypeFieldValues() accessor rather than duplicating the map in the component.
Form reconfiguration on type change is already handled reactively by v9's
type-bind machinery, so only the save is restored here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix type-bind match to handle multi-segment controlling fields

Convert metadata to underscore notation instead of replacing only the
first underscore in the field id (addresses Copilot review).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…labels (#1339, #1337) (#1400)

ADAPT port of the dtq-dev ROR display cluster (FE side of the ROR feature):
- a9fc7f5 (#1339): authority-based publisher search links + ROR icon on the
  CLARIN item page untyped field and the search-result card.
- 0ac3e53 (#1337): form.other-information.ror-id / .location i18n labels.

Runtime:
- clarin-shared-util.ts: new buildAuthoritySearchFilter(searchType, mdValue) centralises
  the authority-vs-equals operator logic; loadItemAuthors refactored onto it
  (behaviour-preserving). isEmpty added to the empty.util import.
- clarin-generic-item-field: getLinkToSearch rewritten to resolve the full MetadataValue
  via allMetadata()[index] (so a ROR authority is used); getMetadataValue removed. HTML:
  ROR <img> added inside the type==='search' anchor, guarded by
  mdValue.authority && fields includes dc.publisher/creativework.publisher.
- clarin-item-box-view: publisherMd via allMetadata(['dc.publisher','creativework.publisher']);
  hasPublisherRorAuthority flag; publisherRedirectLink via the helper. HTML: [href]->[attr.href]
  (no transient broken link before assignBaseUrl resolves) + @if(hasPublisherRorAuthority) ROR icon.

v9 adaptations:
- Templates rewritten to @if (v9 is @if-migrated; fork used *ngIf); BS4 ml-1 -> BS5 ms-1.
- item-box-view: did NOT drag in the fork pre-image's metadataLangToBcp47 line (that comes
  from the not-yet-ported a11y commit d154682).
- ror-icon.svg: kept the existing vanilla blob (24735df), NOT the fork blob (add/add).
- i18n: location BEFORE orcid, ror-id AFTER orcid (en) / AFTER other-names (cs), commented-
  English + Czech pair convention. Hand-merged into the CLARIN-modified json5.
- Specs (all 3 dropped by the v9 squash) restored & adapted to standalone TestBed:
  clarin-shared-util.spec.ts (7 tests), clarin-generic-item-field.component.spec.ts
  (7 tests incl. the AC5 negative-icon rendering test: no ROR icon on dc.subject),
  clarin-item-box-view.component.spec.ts (10 tests, +provideRouter for the standalone
  routerLink). Karma: 24/24 green. lint:nobuild: 0 errors.

Cluster: BE 00501a2db0 (SimpleRORAuthority) is already deployed (merged BE-3 #1381), so the
feature is live-verifiable. Without an authority the FE degrades to the equals fallback.

Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md §5 / a9fc7f5 + 0ac3e53 (FE-3, Vlna 3).
…ssion PATCH (#1301) (#1396)

ADAPT port of dtq-dev commit 50a0e30 onto dtq-dev-9-base.

Problem: the CLARIN license submission section always resolved the in-progress
item via WorkspaceitemDataService.findById and PATCHed the hardcoded
/api/submission/workspaceitems/<id> endpoint, so a reviewer editing a WORKFLOW
item could not load the accepted license nor save a new one.

Fix: replace WorkspaceitemDataService + HALEndpointService with the route-aware
SubmissionService.retrieveSubmission (getActualWorkspaceItem -> getActualSubmissionItem
returning SubmissionObject), send the sections/clarin-license/select PATCH to the
submission object's own _links.self.href (which is /api/workflow/workflowitems/<id>
for workflow items), and add hasSucceeded/null guards on both RemoteData resolutions.

v9 adaptations:
- File lives at submission/sections/clarin-license-resource/ on v9 (not clarin-license/).
- Removed now-unused imports (WorkspaceItem, WorkspaceitemDataService,
  HALEndpointService, rxjs find); added SubmissionService + SubmissionObject in
  eslint-sorted positions. .toPromise() kept (compiles on v9-base; diff-minimal).
- Spec: v9-base had dropped this spec entirely; rewritten from the dtq-dev spec into
  v9 standalone TestBed style (imports[] not declarations, standalone TestComponent).
  Karma: 2/2 green incl. the assertion that sendRequest PATCHes the submission
  self link (route-aware, works for workflow items).

Not pulled in: BE twin 8320889f2c (#1327) — separate BE-4 tranža. Happy path
(claimed task) already works on today's v9-base BE via OPERATION_PATH_SECTIONS ->
ClarinLicenseResourceStep; full 403/404 error-semantics parity is the BE-4 pass.

Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md §5 / 50a0e30 (FE-4, Vlna 3).
…nsole warnings (#1494)

ensureSelfLink() stripped embed params from the requested url only, so every
request that embeds a subresource compared a url the frontend had normalised
against a self link the API had not, and reported a mismatch it had created
itself. The API also percent decodes values it has no reason to escape - ':' and
'/' are legal in a query component - which looked like another difference.

Both sides are now brought to the same form before the comparison: embed params
stripped, values percent decoded (urlPartsDiffer, decodeUrlParts,
isUnexpectedSelfLink). A difference in an actual value still warns.

The second cause is the call sites that asked for 9999 / 10000 / 1000 elements to
mean "everything". Spring Data REST caps a page at spring.data.rest.max-page-size,
which DSpace leaves at its default of 1000, so those requests never returned more
than 1000 rows anyway - they only made the request claim something the API does
not honour and left the self link legitimately different from the requested url.
MAX_PAGE_SIZE is added next to FindListOptions and used wherever a caller needs
everything.

The condition that rewrites _links.self is untouched, so the url a response is
cached under stays exactly as it was; only whether we warn changes.

Adds the first spec this service has had: 17 cases covering both what is now
tolerated and what still has to warn.

v9 notes:
- The text is the 9.3-native original (customer/jcu ca40c3b), which is the same
  change already formatted for the v9 eslint import rules. registry.service.ts was
  resolved by hand to keep the CLARIN MetadataBitstreamDataService import.
- The six CLARIN-only call sites were hand-applied from d0897c2:
  clarin-zip-download-page, clarin-all-licenses-page, item-license-mapper,
  clarin-item-versions-field, clarin-item-box-view, section-license. The #1400
  (ROR) and #1396 (route-aware submission PATCH) hunks in the last two files are
  untouched.
- Two call sites exist only on v9 and therefore only in the jcu commit:
  filtered-items.component.ts (4 x 10000; the file does not exist on dtq-dev) and
  relationship-type-data.service.ts (dtq-dev still pages it with elementsPerPage 20).
- browse-by-geospatial-data.component.ts is deliberately left alone: its 99999 is a
  Discovery facet limit, not a Spring Data REST page size.

Card FE-06 (tranche T1).

Source: d0897c2 (dtq-dev PR #1415)
Adapted-from: ca40c3b (customer/jcu PR #1440)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the API reduces a page size (#1495)

ensureSelfLink() logs one wording for every self link that differs from the url it
was requested with, and that wording ends with "This could mean there's an issue
with the REST endpoint". For a page size the API reduced, that points at the wrong
place: the API did exactly what its contract says, the caller asked for a bigger
page than it will ever serve. Whoever reads the console goes looking at the
backend for a frontend mistake.

Give that case its own message:

  The request for '.../bundles?size=9999' asked for a page of 9999 elements, but
  the REST API served 1000. Ask for at most MAX_PAGE_SIZE elements

It applies only when the two urls are otherwise identical, so an accepted
reduction cannot mask a wrong page or sort. Anything else keeps the generic
message, including a page that came back larger than requested.

- dspace-rest-response-parsing.service.ts: isUnexpectedSelfLink() is replaced by
  selfLinkWarning(), which returns the message to log or undefined when there is
  nothing to report, plus the getPageSize()/withoutPageSize() helpers and the
  PAGE_SIZE_PARAM they match on. The self link is normalized either way, as
  before, so the url a response is cached under does not change - only whether and
  how we warn.
- dspace-rest-response-parsing.service.spec.ts: 17 cases to 19. The reduced page
  size case now expects the specific wording; one new case pins that a reduced
  size is recognised alongside other params, another that a page differing as well
  falls back to the generic message. It also drops a stray note left in the spec
  by #1415.

v9 notes: this is the 9.3-native original (customer/jcu 382a721) applied
unchanged. Its two files were byte-identical with this branch after #1415, so the
cherry-pick was clean, and its changed lines are identical to those of the dtq-dev
commit 51796ec (the two differ only in the untouched import block's formatting).

Stacked on ufal/port-1415-9-base (card FE-06 / PR #1415) - #1490 rewrites the
helper that #1415 introduces.

Card FE-01 (tranche T1).

Source: 51796ec (dtq-dev PR #1490)
Adapted-from: 382a721 (customer/jcu PR #1448)
(cherry picked from commit 382a721)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…om sections.license (#1497)

TESTS-ONLY completion of the #1292 port. The runtime hunk (the CLARIN license
section PATCHes /sections/clarin-license/select instead of sections.license)
arrived with the v9 squash, and the karma spec that pins the PATCH path arrived
with #1396 (card FE-25) - section-license.component.spec.ts already asserts
body[0].path === '/sections/clarin-license/select'. Nothing to add there.

What was still missing is the e2e half. #1292 also hardened
cypress/e2e/submission-ui.cy.ts so that the CLARIN licence section's card
header is asserted to carry the green status icon (.fa-check-circle.text-success,
15 s timeout) when no file has been uploaded. That file does not exist on the v9
base - the fork's submission-ui.cy.ts was replaced by the vanilla
submission.cy.ts - so the step is adapted into submission.cy.ts instead.

v9 notes:
- The step is conditional on div[id="section_clarin-license"] existing (decision
  D-03). CI e2e runs against a vanilla backend image, which does not ship the
  clarin-license submission step, so an unconditional step would fail there. On a
  CLARIN backend the section is present and the assertions run.
- It is placed in the first test ("should create a new submission when using
  /submit path"), which uploads no file - the same precondition as the fork's
  "should not show validation warnings when no file is uploaded (metadata-only)"
  test the hunk came from.
- The fork's cy.wait(1000) settle and the 15 s retry timeout are kept verbatim.
- No cy./expect() step is removed or weakened (guard X6).

Card PB-08 (tranche T3).

Source: 0645e7e (dtq-dev PR #1292)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…igation after bitstream upload (#1498)

ADAPT completion of the #1280 port. The v9 squash carried 3 of the 9 files
(clarin-files-section, preview-section runtime, and nothing else); the other six
are byte-identical with vanilla dspace-9.3 on the base, i.e. the fork hunks were
never applied. Without them the cache invalidation that did land is inert: the
item resolver still asks for a cached item and never re-requests a stale one, so
a file uploaded or deleted is not visible until a hard refresh.

Only the missing hunks are applied here; clarin-files-section.component.ts and
preview-section.component.ts are verified line by line as already complete and
are not touched.

- item.resolver.ts: findById(..., useCachedVersionIfAvailable=true,
  reRequestOnStale=false -> true). This is the load-bearing hunk - every
  setStaleByHrefSubstring below does nothing without it.
- upload-bitstream.component.ts onCompleteItem: removeByHrefSubstring ->
  five setStaleByHrefSubstring calls (bundle bitstreams endpoint, item bundles
  endpoint, item self link, and metadatabitstreams/search/byHandle in both the
  encoded and the raw handle form).
- item-bitstreams.component.ts submit(): the same invalidation after a delete.
- bitstream-breadcrumbs.service.ts: hasValue(bitstream.bundle) /
  hasValue(bundle.item) guards, so a stale re-request that resolves an
  incomplete bitstream cannot throw in the breadcrumb resolver.
- edit-bitstream-page.component.ts: the ngOnInit guards (bundle$,
  primaryBitstream$, item$, format$ each fall back to of(undefined); the
  combineLatest subscriber only assigns what it actually received) and
  setIiifStatus operating on its bitstream argument behind a null guard.
- upload-bitstream.component.spec.ts: +5 tests, getBundlesEndpoint stub,
  mockItem._links.self, removeByHrefSubstring -> setStaleByHrefSubstring spy.
- preview-section.component.spec.ts: restored (dropped whole by the squash),
  converted to a standalone TestBed.

v9 notes:
- rxjs `of` instead of the fork's `observableOf` alias (eslint
  dspace-angular-ts/alias-imports).
- item.resolver.ts is a functional ResolveFn on v9; the flip is the same
  positional argument.
- item-bitstreams.component.ts already imports hasValue, so the source's import
  hunk is a no-op here.
- The vanilla-9 onSubmit() bundle$/combineLatest([bundle$, bitstream$]) save
  path is deliberately untouched - it is a different local bundle$.
- preview-section.component.spec.ts: declarations -> imports:[PreviewSectionComponent],
  trailing commas and simple-import-sort order for the v9 eslint config. The three
  assertions are the fork's, unchanged.

Card PB-01 (tranche T3).

Source: 3a219e4 (dtq-dev PR #1280)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… spec parity) to the v9 base (#1499)

* Port #1260 to dtq-dev-9-base: License-contract page refactor - restore the dropped spec

TESTS-ONLY. The runtime half of #1260 (list mode without a collectionId,
PaginationService-driven paging, loadAuthorizedCollections, clearPagination on
destroy) is already on the v9 base and matches the source hunk for hunk; the v9
squash dropped the spec file whole. This restores it, so the six behaviours the
refactor introduced are pinned again.

The six assertions are the fork's, unchanged: component creation, collectionRD$,
licenseRD$, hasFailed on a bogus collectionId, list mode loading authorized
collections when the query param is missing, and clearPagination on destroy in
list mode.

v9 notes:
- declarations:[LicenseContractPageComponent, MockNgVarDirective] ->
  imports:[LicenseContractPageComponent]. The component is standalone and imports
  the real VarDirective, so the fork's *ngVar mock directive is dropped rather
  than adapted - keeping it would shadow the directive under test.
- ErrorComponent, PaginationComponent and ThemedLoadingComponent are removed from
  the component's own imports via overrideComponent and NO_ERRORS_SCHEMA is added
  there, because two of the tests render list mode and PaginationComponent pulls
  in a provider graph this spec has no business standing up.
- `of` instead of the fork's `of as observableOf` (dspace-angular-ts/alias-imports).
- The getAuthorizedCollection assertion is new and v9-specific: it pins the 6-arg
  signature including the mandatory searchHref 'findSubmitAuthorized', which is
  exactly the one place the v9 runtime differs from the source commit.
- routeStub/collectionService/paginationService are const (the fork's `let` +
  later assignment trips prefer-const on v9).

Card PB-06 (tranche T3).

Source: f2fe67b (dtq-dev PR #1260)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Port #1241 to dtq-dev-9-base: Static pages - SSR render and 404 - restore the two dropped specs

TESTS-ONLY. The runtime half of #1241 is on the v9 base: HtmlContentService
resolves namespace-aware URLs and, under SSR, absolute ones from the express
REQUEST; StaticPageComponent tracks contentState and calls
ServerResponseService.setNotFound() so a missing static page really answers 404.
The v9 squash dropped both spec files whole. This restores them.

html-content.service.spec.ts - the fork's 5 tests plus 5 that pin the v9-only
half of the service:
- namespaced URL for the default locale, localized URL for a non-default locale,
  fallback localized -> default (twice, with and without a trailing slash on the
  namespace), and getHtmlContent swallowing a failure into ''.
- NEW: on the server with a REQUEST the request URL is absolute
  (https://host/repository/...), on the server without one it stays relative, in
  the browser the REQUEST is ignored, a URL that already carries the namespace is
  not prefixed twice, and an absolute http(s) URL is left alone.
  The first of these is the assertion the card asks for: it fails loudly if
  REQUEST is imported from the wrong module, because @optional() injection would
  then silently hand the service undefined and the URL would stay relative. The
  "no REQUEST" test is its counterpart, so the pair cannot both pass by accident.

static-page.component.spec.ts - the fork's 19 tests unchanged in substance:
creation, content load, file name from the route, the five OAI-link rewrite
cases, the five contentState cases (including 'not-found' plus setNotFound()),
the two change-detection cases and the four link-handling cases.

v9 notes:
- LocaleService.getCurrentLanguageCode() returns an Observable on v9, not a
  string, so the stub returns of(languageCode). That also puts a microtask
  between the call and the HTTP request, so the two non-fakeAsync tests await a
  settle() before expectOne; the fakeAsync ones tick(). Without this the fork's
  spec would fail here - it is a real behavioural difference, not a flake.
- HttpClientTestingModule -> provideHttpClient() + provideHttpClientTesting()
  (the module is deprecated on Angular 20; this is what the other v9 specs use).
- declarations:[StaticPageComponent, ClarinSafeHtmlPipe] ->
  imports:[StaticPageComponent]; the pipe is one of the component's own imports.
  RouterModule and ThemedLoadingComponent are removed from those imports via
  overrideComponent with NO_ERRORS_SCHEMA, because the loading and 404 branches
  render <ds-loading> and routerLink and would otherwise need the theme and
  router provider graphs.
- submission-section-cc-licenses.component.spec.ts is deliberately NOT touched:
  the third hunk of #1241 added fakeAsync/tick(350) around a debounceTime that
  the v9 component does not have (grep debounceTime -> 0).

Card PB-07 (tranche T3).

Source: f8495ea (dtq-dev PR #1241)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…description` instead of `dc.description.abstract` (#1291) (#1501)

The search result list and grid elements read the item description from
dc.description.abstract. LINDAT items carry their description in dc.description,
so both views rendered nothing where the description belongs.

Both templates now read dc.description in the guard and in the binding (4
substitutions across the two files). The trade-off is deliberate and was made in
#1291: an item that only has dc.description.abstract now shows no description in
these two views. The two new negative specs pin exactly that, so the trade-off
cannot be undone by accident.

- item-search-result-grid-element.component.html / .spec.ts: the @if guard and the
  [innerHTML] binding switch key; mockItemWithAbstractOnly is added and passed as a
  new 5th argument to getEntityGridElementTestComponent. That parameter is optional,
  so the six entity-group grid specs that call the helper with four arguments
  (journal, journal-issue, journal-volume, org-unit, person, project) keep compiling
  unchanged.
- item-search-result-list-element.component.html / .spec.ts: same key switch plus a
  mockItemWithAbstractOnly and a negative describe block.

v9 notes: the source hunks are *ngIf; both templates on this branch have already
been migrated to @if, so the guards were adapted in place - same condition, same
two metadata keys per file. The list spec uses `of` where the source uses
`observableOf`, and the new mocks follow this branch's trailing-comma style. The
custom theme's item-search-result-list-element reuses the base templateUrl, so no
theme override needed changing; the grid element has no theme override.

Card PB-05 (tranche T3).

Source: 3a95a85 (dtq-dev PR #1291)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
milanmajchrak and others added 27 commits September 23, 2026 18:35
…1535)

Bring back the six spec files that src/app/epic-handle and
src/app/change-submitter-page lost in the v9 standalone migration, including the
Delete Functionality block and the only coverage of the alphanumeric prefix
validator. Adaptations are harness-only: the components under test move from
declarations to imports, and the two specs whose component now really imports
its children drop those children with overrideComponent to keep the original
isolation.

Ported from dataquest-dev/dspace-angular 53c3a56

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…9 upgrade (#1527)

Seven spec files whose components and services are alive on the v9 base.
Test harness only; no production code changes.

Source: 4d8aa22
Source: 40997ba
Source: c84628f
Source: eacad5b
Source: 06fb628
Source: eef127f

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Restore dropped CLARIN unit specs on the v9 base

Restores the 14 clarin*.spec.ts files that the v9 port dropped, adapted to
Angular 20 standalone testing. Ported from dspace-angular dtq-dev
53c3a56.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Use neutral test data in CLARIN submission section specs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Milan Majchrák <minptai7@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restores admin-menu and handle-page, reading fixtures from Cypress.env
instead of the TEST_* constants the fork exported. Ported from dspace-angular
dtq-dev 53c3a56.

tombstone.cy.ts is left out: it needs withdrawn items that only the CLARIN
data set carries, and the e2e job here runs the vanilla image.

Co-authored-by: Milan Majchrák <minptai7@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…der (#1555)

* Give the item share links an accessible name

The Facebook and Twitter share anchors in the CLARIN ref box render as
font-awesome glyphs with no text, no aria-label and no title, so a screen
reader announces a bare link and reads the raw sharing URL. Label both from
i18n, and label the featured-service dropdown toggle, which is empty the same
way. Guard the names with a spec that also runs axe link-name/button-name over
the rendered component.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Read the real catalogue in the share-link accessible name spec

The spec fed the translate service its own dictionary, so dropping the keys
from en.json5 left it green while production rendered the bare key as the
accessible name. Load src/assets/i18n/en.json5 from the karma server instead
and assert the keys are there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep the share-link spec honest about what the name must say

Restore the assertion that the label says what the link does, not only which
network it points at, and fail loudly rather than with seven undefined
dereferences when the catalogue cannot be read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make the accessibility gates wait for the client render

SSR delivers the themed wrapper without its contents, so should('be.visible') is
satisfied on an empty subtree and axe finds nothing in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1559)

* Fail the nightly run when the Playwright suite produces no verdict

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* End the Playwright step before the job limit cancels it

A gate job cannot turn the run red: once a job is cancelled, the run conclusion
is latched to 'cancelled'. The suite step now ends itself at 55 minutes instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Milan Majchrak <minptai7@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CLARIN licence menu still used the Bootstrap 4 data-toggle attribute.
No Bootstrap JS is bundled, so the menu never opened. It now uses
ng-bootstrap's dropdown directives. The licence list link gets a real
href through RouterLink, and Space no longer scrolls the page on the
licence details toggle.

The dispatchDeposit specs stubbed store.select for every selector, so a
wrong submission id stayed green. They now run the real selector against
a state literal, and a new spec checks that another submission's notice
is ignored.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…command, unique ids, request-a-copy token on downloads (#1570)

* CLARIN-DSpace v9/Fix the item page file panel, download command and version row ids

The file preview panel heading uses #31708f on #d9edf7 (4.53:1, was
3.32:1), and its axe check runs with color-contrast on, over the open panel.
Space on the Download, Preview, close and folder controls no longer scrolls
the page. The curl command sends the file name as the filename query
parameter. The version list uses clarin-version-row- ids, so it no longer
repeats the ids of the version table administrators see.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* CLARIN-DSpace v9/Give each bundle and pagination its own dropdown ids

The pagination dropdown on the edit bitstreams page and in ds-pagination now
carries the bundle name or the pagination id in its element ids and
aria-labelledby, so two bundles or two paginations on one page no longer
share ids. Ported from dtq-dev commit 6628aaf.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* CLARIN-DSpace v9/Pass the request-a-copy access token to file downloads

A person who opens an approved request-a-copy link lands on the item page
with the token in the route data. The Download control, the video preview
and the Download all (ZIP) button dropped it, so the download gate asked
for a login again.

They now add accessToken as a query parameter when the approved, unexpired
request grants that file (or all files). Without a grant nothing changes.

The ZIP download page reads only dtoken today, so on the ZIP link the
token has no effect until that page reads accessToken too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* CLARIN-DSpace v9/Bind the pagination ids the same way as their labels

The pagination dropdown ids used interpolation and their aria-labelledby
used a property binding. Without a pagination id the first rendered
"paginationControls-" and the second "paginationControls-null", so the
label pointed nowhere. Both now use the same binding, like the existing
"p-" id in that template.

The version list spec now also ignores the version table's alert and
pagination elements, which it leaves out on purpose.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* CLARIN-DSpace v9/Drop the unused ZIP token and test the pagination ids

The ZIP download page reads only dtoken, so the access token on the
Download all (ZIP) link did nothing. The link goes back to what it was.
The grant rule now has one caller, so it moves into the file panel.

A new pagination spec renders two paginations on one page. It checks that
their ids differ and that each aria-labelledby points into its own
pagination.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The community list reloads with new node objects, but CDK keeps the
object a row got when it was first inserted and asks the tree control
about it. The tree control read isExpanded from that object, so after a
reload a collapsed row still read true and a newly expanded one false.
It now answers by node id from the component's expanded nodes. The arrow
keys still move only the control's own model, so they cannot make a row
announce an expansion.

The spec stub now builds new node objects on every reload, like the
real service.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
)

* Restore the remaining CLARIN specs and e2e tests

- EpicHandleDataService spec from dtq-dev, adapted to provideHttpClient
- processClarinAuthorization tests in the download-page spec, next to the request-a-copy guards
- angular.json spec that fails when the licence selector scripts leave build.options.scripts
- testLinkNamesOnPage waits for the client render like testA11y
- tombstone e2e spec that creates and deletes its own withdrawn and replaced items
- app.menus.spec.ts comments explain the code without tracking ids

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop issue numbers and tracking ids from code comments

The comments in robots.txt.ejs, the scrollable dropdown spec, the licence label serializer spec
and the upload-files spec still say why the code is there, without the reference.
The epic-handle URL pattern test gets an expectation, which this karma config requires.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Delete every item the tombstone e2e test created

Keep the id as soon as the POST returns, so after() also removes an item whose withdraw failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Make the tombstone REST setup and the backend links test deterministic

- clear every CSRF cookie before creating the test's own, so the backend never compares
  the header against its own cookie on /server
- attempt every DELETE in after() and fail afterwards if one was refused
- the backend _links fixture uses a URL the fallback cannot build

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The full suite against 8603 takes about 80 minutes, so the 60 minute limit cut it off before webkit ran.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Restrict main search page to items only

Add a forcedDsoTypes input to the (themed) SearchComponent and set it to
items on the main search page, so communities and collections no longer
appear in /search results. Backend keeps the inclusive discovery config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Restrict community/collection and configuration search to items

The community and collection page search and /search/:configuration now
pass forcedDsoTypes=[ITEM], like /search. Subcommunities and collections
are indexed under their parent community, so they showed up in the
community search.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop the forced dsoTypes on search

The rule that search shows items only moves to the backend discovery
configuration, so it lives in one place. The FE no longer forces
dsoTypes on the search page, the community and collection search or
the /search/:configuration route.

This reverts commits 302bfd2 and 25d6eac.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Search page asks for the items discovery configuration

The main search page now sends configuration=items. The backend decides
that this configuration returns items only. A configuration in the URL
(?configuration= or /search/:configuration) still wins.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ics when not configured (#1576)

* CLARIN-DSpace v9/Licence picker works from the keyboard; hide Statistics when not configured

The licence options in the submission form are now ngbDropdownItem
buttons in a div menu, so the arrow keys move between them and Enter or
Space picks one. The menu was a ul that held a div and li items.

The full item page shows the Statistics button only when
statistics.baseUrl and statistics.endpoint are set, like the simple
item page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Return focus to the licence field after a choice

After a licence is picked from the menu, focus goes back to the licence
field. Before, a keyboard choice left the focus on the page body.

The full item page spec now also checks that the Statistics button stays
hidden when only statistics.baseUrl is set.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The login route used notAuthenticatedGuard, which sends a logged-in user
to page not found. So nobody saw the panel the login page shows to a
logged-in user. dtq-dev had no guard on this route. Register, forgot
password and the external login routes keep the guard.

A new spec checks that login has no notAuthenticatedGuard and that
register still has it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…by link (#1579)

* Return to the item after editing a bitstream opened by link

The bitstream page followed 'bundle' twice, once for the item and once
for the primary bitstream. LinkService sets bitstream.bundle once per
entry, so the second one won and bundle.item was never set. Opening the
edit page by a direct link and saving then went back to the previous
page instead of the item's edit page.

The split was there because addNestedEmbeds built each nested embed on
top of the previous sibling, so one bundle entry sent
embed=bundle/primaryBitstream/item instead of embed=bundle/item. Each
sibling now starts from the parent path, and the resolver uses one
bundle entry again, as in vanilla.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Assert the exact embed list of the bitstream page

A prefix match on embed=bundle/primaryBitstream also passes on
embed=bundle/primaryBitstream/item, and embed=bundle/item also passes on
embed=bundle/item/primaryBitstream. Compare the whole list instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
)

* Show which attempt a retried Cypress test passed on

Cypress retries failed tests up to 2 times in run mode, and the default
spec reporter does not say which attempt passed. An after:spec hook in
cypress.config.ts now prints one FLAKY line per test that needed a retry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Guard undefined test title in the after:spec hook

A throw inside after:spec could take down the run's reporting, so fall
back to an empty title instead of calling join on undefined.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Skip three flaky Cypress tests until they are fixed

A global beforeEach skips the tests listed in QUARANTINED_TESTS.
An entry is the spec file name plus the full test title, so the
same title in collection-create.cy.ts still runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Skip the flaky collection-create Cypress test too

Its loading spinner test is the same race as the one in
community-create.cy.ts. Retries rescued it twice in 17 runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…onfiguration (#1580)

* Translate the search results heading for the items configuration

/search asks for the items discovery configuration. The results heading
is translated as <configuration>.search.results.head, and that key was
missing. Add it to en.json5 and cs.json5, with a spec that checks it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Add the items search results heading to the other languages

Copy each language's default.search.results.head value, so the heading
on /search keeps the translation it had with the default configuration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep the items search results heading in English and Czech only

The fork adds new keys only to en.json5 and cs.json5. Remove the key
from the other language files again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1582)

An optional complex field such as Funding or Size has no mandatory
message, but its inner inputs can be required. They got an undefined
required message, so reading the error messages threw a TypeError when
the form announced errors after a save. Use error.validation.required,
the key the server sends for these fields.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nce page (#1578)

* Send the request-a-copy token to the backend whatever refused the download

With an accessToken in the URL, the download page no longer stops at the
licence agreement, the login page or the forbidden page. It redirects to the
content URL with the token, and the backend decides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Test that the licence page stays when there is no access token

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Read the access token on the download page from the URL only

The download route no longer resolves the request-a-copy request.
The page takes the token from the URL and the backend checks it,
so a token the backend does not know ends in its 401 or 403.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep the request-a-copy token through the CLARIN licence page

The download page sends the accessToken with the CLARIN check, hands it
to the licence page and to the expired token page, and adds it to the
content URL next to the dtoken. The login keeps it in the return URL.
The licence page sends it with the manage request, so the e-mailed link
has it too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Return to the item page with the request-a-copy token

When the download link goes by e-mail, the licence page goes back to
the item page. It now keeps the accessToken there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…CC licence, files section, version names, translations (#1584)

* Port missed fork fixes: meta tags, error page link, CC licence patch, files gate, version names

- head-tag.service: add citation_date and the dataset_* meta tags, first author
  only in citation_author, prefer dc.language.iso, add dc.type to citation_keywords
- page-error: home button uses routerLink so it stays under the base href
- cc-licenses: a save response has no "accepted" flag, so it no longer queues
  a second remove of the licence URI
- item page and full item page: hide the files section when local.has.files is "no"
- admin version table: show the version item's title instead of the version number

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Use the existing untitled i18n key in the version table

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Port fork wording for i18n keys that vanilla 9.3 left unchanged

- cs: CLARIN title prefix, "Stáhnout" for withdraw instead of "Odstranit"/"Odebrat",
  item page link and identifier labels, eperson email, sub-community header,
  licence-not-granted message, Solr search core title
- en: feed.description "RSS feed"

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GitHub runner's Chrome moved from 154 to 155 on 2026-10-15 and
since then 5 to 7 random Cypress specs time out in every run. Install
Chrome for Testing with browser-actions/setup-chrome and run Cypress
with browser chrome-for-testing, as upstream dspace-angular does.
When a job fails, the backend docker compose logs are uploaded as an
artifact.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… too (#1641)

Port of dtq-dev PR 1615 (commit dd5a17b), written for the 9.x component.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
v6 (and v7 < 7.4.4) uses got@11 for its wait-on ping. On the runner's
bundled Node >= 24.20 a refused connection is raised as
ERR_SOCKET_CLOSED_BEFORE_CONNECTION and aborts the step instead of being
retried. Fixed upstream in v7.4.4 (cypress-io/github-action#1873).

Co-authored-by: jm <jm@maz>
(cherry picked from commit 44dd291)
CLARIN-DSpace v9: bump cypress-io/github-action to v7.4.4
…ors (#1660)

* Convert two components to LF line endings

Both files mixed CRLF and LF lines. .gitattributes requires LF for *.ts,
so any edit converted the whole file. No content change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CLARIN-DSpace v9: request-a-copy flag on item pages, licence page errors

Item pages list files through ds-file-description, which always went to
the licence gate, so item.bitstream.enableRequestACopyLink had no effect
there. It now uses the same rule as FileDownloadLinkComponent: with the
flag on, a user who cannot download but can request a copy goes to the
request-a-copy form.

The licences page waited for a successful response only, so a failed
request left the spinner running. It now stops the spinner and shows an
error.

Review findings of dspace-customers issue 1101.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test that the preview section passes the item to file descriptions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants