Skip to content

UoE/WP2-2026-07-09 bugfixes - #6

Merged
dspeed2 merged 22 commits into
UoEMainLibrary:datashare-UoEMainLibrary-dspace-8_xfrom
dataquest-dev:sync-prs-2026-07-09
Jul 9, 2026
Merged

UoE/WP2-2026-07-09 bugfixes #6
dspeed2 merged 22 commits into
UoEMainLibrary:datashare-UoEMainLibrary-dspace-8_xfrom
dataquest-dev:sync-prs-2026-07-09

Conversation

@milanmajchrak

@milanmajchrak milanmajchrak commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

This PR contains the following fixes:

  1. The DOI metadata field was not displayed when the item did not contain a DOI identifier.
    Fix: Show DOI registration in progress on the Item View when the DOI has not been registered yet.
  2. The submission froze while in progress.
    Fix: Propagated the fix from Vanilla. When select options take more than 15 seconds to load, an error message is shown asking the user to try again.
  3. Show checksum information and hide the bitstream description.

jr-rk and others added 22 commits July 9, 2026 14:05
Replace the per-file "Format" line in the full item view file listing
with a "File checksum" line (algorithm + value). Add the checkSum field
to the Bitstream model so the REST value deserializes, and add the
item.page.filesection.checksum i18n key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the full item view checksum dt/dd in *ngIf="file?.checkSum" and render
only the checksum value (drop the algorithm prefix), so no stray colon shows
when a bitstream has no checksum. Propagate item.page.filesection.checksum to
all locale files with an English placeholder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match DSpace/dspace-angular@main (commit 4f72074): use the exported
ChecksumInfo interface and `checkSum: ChecksumInfo` positioned after
`description`, replacing the inline anonymous type, to minimize conflicts on a
future DSpace 9 migration. Display and i18n key are unchanged (upstream renders
no checksum row in the full item view).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UoE instance only ships English; other locale .json5 files aren't
maintained here. Revert the checksum key addition to all 27 non-en
locales, keep it in en.json5 only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
file is guaranteed by *ngFor and checkSum is truthy inside the *ngIf
guard, so use plain file.checkSum / file.checkSum.value to match the
surrounding template style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chMap -> concatMap)

Backport of the save-effect change from upstream PR DSpace#5145
(commit a3c14c5, first released in DSpace 10.0; not present in 8.x/9.x).

The four submission save effects used switchMap. When two save actions of the
same type overlap (common during editing: unguarded section/upload/save-for-later
saves, save-on-change on validation errors, or an enabled autosave timer),
switchMap cancels the in-flight save's inner observable AFTER it dispatched
StartTransactionPatchOperationsAction (commitPending=true) but BEFORE its response
handler runs. Commit/Rollback are dispatched only inside that cancelled observable,
so commitPending stays true forever; the next save is then dropped by the
take(1)+filter(!commitPending) guard in submitJsonPatchOperations, no
SAVE_*_SUCCESS/ERROR is emitted, savePending stays true, and the form is stuck on
"Saving..." until the user reloads (losing unsaved metadata).

concatMap serializes the save effects so each inner observable completes
(dispatching Commit/Rollback and clearing commitPending) before the next save runs,
eliminating the race.

Only the four save effects are changed (saveSubmission$, saveForLaterSubmission$,
saveSection$, saveAndDeposit$), matching upstream. deposit/discard/updateSection
intentionally keep switchMap. The improved catchError from DSpace#5145 is not backported
(it depends on parseErrorResponse and action signatures not present in 8.3).

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

Fixes a class of "spinner spins forever, user must reload and loses typed
metadata" bugs in the submission form. A component sets a loading/searching flag,
loads data via a RemoteData pipe, and resets the flag ONLY on the success path
using getFirstSucceededRemoteData()/...Payload()/...ListPayload(). DSpace turns
HTTP errors into a NON-throwing failed RemoteData (state=Error), and the
store-backed RemoteData observable never completes, so on a failed OR slow/
never-responding request the success-only operator never emits, the component's
catchError (which only catches thrown errors) never fires, and the spinner stays
forever. There is also no HTTP request timeout anywhere in the app.

Inherited from vanilla DSpace 8.3 and NOT fixed upstream (the same pattern is
still present on main/10.x), so there is no backport available -- this is a new
hardening fix, following the same getFirstCompletedRemoteData approach upstream
used for the whole-form loading fix (DSpace#4060).

For each affected load: switch getFirstSucceededRemoteData* to
getFirstCompletedRemoteData() (emits on success AND failure, then completes),
extract the payload failure-safely, add timeout({ each: 30000 }) for
never-responding requests, and reset the spinner flag in finalize() so it always
clears. On failure/timeout the control shows an empty result instead of hanging.

Sites fixed (all inherited from 8.3):
- sections/form/section-form.component.ts (Describe section) -- bounded 30s
  timeout so it never spins forever; kept getFirstSucceededRemoteData for the
  item follow-link to avoid dereferencing an undefined submission object.
- ds-dynamic-form-ui/models/list/dynamic-list.component.ts (radio/checkbox vocab, e.g. dc.type)
- ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts (vocabulary dropdowns)
- ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts (authority typeahead)
- ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts (keyword/tag authority)
- ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts (lookup authority)
- vocabulary-treeview/vocabulary-treeview.service.ts (hierarchical vocabulary tree)
- collection-dropdown/collection-dropdown.component.ts (collection picker; parentCommunity was hanging reduce())
- submission/form/collection/submission-form-collection.component.ts (change-collection selector)
- sections/cc-license/submission-section-cc-licenses.component.ts (CC-license section)
- ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts (relation chips on edit)

Verified locally: `tsc --noEmit -p tsconfig.app.json` and `eslint` both pass with
0 errors on the changed files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deterministic reproduction on a local DSpace 8.3 stack (published dspace-8_x-test
backend images + this frontend). A Playwright script simulates a failing/slow
controlled-vocabulary endpoint by returning HTTP 500 for
/api/submission/vocabularies/{name}/entries, then opens the "Type" dropdown on a
new submission's Describe form.

- before: getFirstSucceededRemoteData ignores the failed RemoteData, so the "Loading..."
  indicator stays visible forever (measured stuck for the full 14s observation window).
- after: getFirstCompletedRemoteData + finalize handles the failed RemoteData, the
  spinner clears immediately and the dropdown shows "No results found".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deterministic reproduction on a local DSpace 8.3 stack. A Playwright script
delays every submission PATCH by ~3s (models a slow save) and fires two
overlapping autosaves (two edits of the autosave-on-change dc.title field).

- before (base, switchMap): the second save cancels the in-flight first save
  mid-transaction; commitPending is orphaned true; the second save is dropped by
  the take(1)+filter(!commitPending) guard (only 1 PATCH ever leaves the client);
  savePending never resets -> footer stuck on "Saving..." for the full 18s window.
- after (this PR, concatMap): the saves are serialized; the first completes and
  clears commitPending before the second runs; "Saving..." clears within ~4s.

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

Second reproduction covering the slow/never-responding path (not just HTTP 500).
The vocabulary /entries request is held open forever (models a hung socket).

- before (no timeout): "Loading..." stays stuck past 45s (never recovers).
- after (timeout({each:30000})): the spinner clears ~30s after the request stalls
  and the dropdown shows "No results found".

Clips are sped up ~3x. Confirms the timeout() backstop half of the fix, which the
HTTP-500 reproduction did not exercise.

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

The DataShare simple item view renders the DOI ("Persistent Identifier") field
via ds-item-page-uri-field, which filtered dc.identifier.uri for values starting
with https://doi.org. The whole field wrapper was gated behind *ngIf="hasDoiLink",
so when a record's DOI has not been registered yet (the DOI value is only written
to dc.identifier.uri by the backend AFTER the doi-organiser scheduled task
registers it), the field was hidden entirely and users saw no indication that a
DOI exists or is coming.

Restore the previous behaviour where the DOI field is always shown (with an empty
value while registration is pending):

- MetadataUriValuesComponent / ItemPageUriFieldComponent: add an opt-in `doiField`
  input. In DOI mode the field wrapper is always rendered (hideIfNoTextContent =
  false) and only https://doi.org values are shown as links. When false (default)
  the upstream generic behaviour is kept, which also repairs non-DOI URI fields in
  the base theme that were unintentionally hidden by the previous global filter.
- datashare untyped-item: opt the DOI field into `[doiField]="true"`.

Test-driven: added specs for the pending (no DOI yet), registered, and empty cases,
plus the default (non-DOI) URI behaviour.

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

Follow-up to the loading-spinner hardening:
- Lower the request timeout backstop from 30s to 15s on all the hardened loads
  (a 30s spinner is too long for an interactive dropdown).
- On a failed OR timed-out controlled-vocabulary / authority lookup, show a
  dismissible error toast ("Something went wrong while loading the options.
  Please try again.") instead of silently showing an empty "No results found",
  so the user knows to retry. Added a shared notifyVocabularyLoadError() helper on
  the DsDynamicVocabularyComponent base (via inject()) and wired it into the
  scrollable-dropdown and lookup loads. A genuinely empty (succeeded) result is
  unchanged -> still "No results found", no error toast.
- Harden dynamic-onebox vocabulary$ (findVocabularyById): getFirstSucceededRemoteDataPayload
  -> getFirstCompletedRemoteData, so a failed vocabulary-metadata lookup resolves
  to "not hierarchical" (no tree button) instead of leaving isHierarchicalVocabulary$
  pending forever. Null-safe guards on result?.hierarchical and vocabulary?.preloadLevel.

tsc --noEmit and eslint pass with 0 errors. Verified in the local repro: on a 500,
the Type dropdown now clears immediately, shows "No results found", AND a red
"Something went wrong ... Please try again." toast appears.

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

The after clip now shows the updated recovery: on a failed vocabulary load the
Type dropdown clears immediately, shows "No results found", and a dismissible
"Something went wrong ... Please try again." error toast appears.

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

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

Address review nits on the pending-DOI fix without changing behaviour:

- Merge the doiField and default template branches into one ds-metadata-field-wrapper
  ([hideIfNoTextContent]="!doiField"), removing the duplicated link markup.
- Add a `doiValues` getter that filters mdValues to https://doi.org values (restoring the
  `typeof value === 'string'` guard). In DOI mode the *ngFor and the `!last` separator are
  computed against this visible-DOI subset, so a trailing non-DOI value can no longer emit a
  stray separator after the last DOI.
- Tighten the registered-case spec to assert the label header, and add a regression spec for
  multiple DOIs followed by a non-DOI value (only one separator between the DOIs).

Verified locally: metadata-uri-values + item-page-uri-field specs 23/23 green, lint clean,
build:prod (SSR production build) exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DsDynamicVocabularyComponent base now injects NotificationsService (for the
"something went wrong" error notification on a failed vocabulary load). Its
subclasses' unit tests (scrollable-dropdown, onebox, tag, lookup) did not provide
it, so component construction failed with
"NullInjectorError: NotificationsService -> Store -> No provider for Store"
(NotificationsService is providedIn root and depends on the ngrx Store, which the
isolated TestBeds don't set up).

Provide the existing NotificationsServiceStub in each of the four specs.
Verified locally: those 4 spec files now run 54/54 SUCCESS (were 54 FAILED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The before/after clips are now hosted on the separate 'pr-repro-media' branch and
referenced from this PR's description, so they are no longer part of the code diff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The before/after clips are now hosted on the separate 'pr-repro-media' branch and
referenced from this PR's description, so they are no longer part of the code diff.

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

The DOI field decides which dc.identifier.uri values are DOIs by matching the DOI
resolver base URL. That URL is a backend setting (identifier.doi.resolver), so read
it from the REST config endpoint instead of hard-coding "https://doi.org" in the UI.

- MetadataUriValuesComponent now fetches identifier.doi.resolver via
  ConfigurationDataService on init (only in doiField mode) and uses it in doiValues.
  Falls back to the DSpace default https://doi.org (DOIServiceImpl#RESOLVER_DEFAULT)
  when the property is not exposed/defined, so it stays correct without any backend
  change.
- Added a spec proving a value matching a custom configured resolver is treated as a
  DOI, and provided ConfigurationDataService stubs to the dependent specs
  (item-page-uri-field, publication, untyped-item).

Requires the backend to expose identifier.doi.resolver in rest.properties.exposed
(separate backend PR); until then the frontend uses the default resolver.

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

Instead of showing an empty DOI ("Persistent Identifier") field while the DOI is still
queued for registration by the scheduled task, show a "DOI registration in progress"
message. Once registration completes and the resolver value appears in dc.identifier.uri,
the DOI link is shown instead.

- metadata-uri-values: in doiField mode, render the item.page.doi.pending message when
  there is no DOI value yet (doiValues is empty).
- add the item.page.doi.pending i18n key to the datashare theme and core en.json5.
- specs: assert the message is shown when pending / no identifier, and NOT shown once a
  DOI is present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dspeed2
dspeed2 merged commit 508977c into UoEMainLibrary:datashare-UoEMainLibrary-dspace-8_x Jul 9, 2026
5 checks passed
dspeed2 pushed a commit that referenced this pull request Jul 27, 2026
* UoE/datashare: check 'Inherit policies' by default on the move screen

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

* UoE/datashare: make clear the Keep-embargo option is about the item's existing embargo

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

* UoE/datashare: add "Keep embargo policies" option to the move-item screen

UoE/datashare: add "Keep embargo policies" option to the move-item screen

* feat: auth gate for admin control of bitstream access conditions

* fix: remove redundant error handling

* fix: patch the JSON patch success handler to clear isSaving before early return

* feat: Hide the admin sidebar for regular authenticated users while preserving it for authorized admin roles

* fix(admin-menu): skip authz calls for anonymous, fix lint

- gate the 5 admin-panel authorization requests behind
  isAuthenticated: anonymous bootstraps (incl. SSR renders)
  make zero authorization calls again
- extract ADMIN_PANEL_FEATURES const with rationale for the
  deliberately excluded features (CanSubmit/CanEditItem/CoarNotifyEnabled)
- rename createAdminMenuIfLoggedIn$ -> createAdminMenuIfAuthorized$,
  fix stale doc comment
- fix lint: import sort/newlines, EOF newline
- specs: anonymous-user cases for resolver + sidebar; fix AuthService
  stub provider (useValue class ref -> useClass)

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

* fix(submission): fail-closed admin check, repair spec teardown crash

- default canEditAccessConditions to false: authorization gates
  must fail closed until the site-admin check resolves
- spec: authorization mock now returns an Observable (was a bare
  spy function -> '.pipe of undefined' crash when the TestComponent
  template ran ngOnInit)
- spec: stub the component's ChangeDetectorRef in the ngOnInit
  tests; rendering the full dynamic form against the mocked
  TestBed caused lazy NullInjectorError (HttpClient) in afterAll,
  which disconnected Karma and killed the whole CI test run
- spec: restore compileComponents() promise chaining
- spec: add explicit admin-path test (form model contains
  accessConditions array for authorized users) and set
  canEditAccessConditions=true in the tests that build the form
  directly

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

* fix(submission): reset isSaving when the save PATCH fails

Address review feedback:
- add error handler to the jsonPatchByResourceID subscription so a
  failed PATCH no longer leaves the modal stuck in the saving state
  (+ regression spec)
- soften the new authorization specs to assert behavior (presence/
  absence of the accessConditions control) instead of exact form
  model shape

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

* Fix admin sidebar visibility regression and reduce scope

- remove one-shot auth sampling and keep reactive auth/admin check in admin sidebar

- gate sidebar on site-admin authorization only (FeatureID.AdministratorOf)

- revert resolver/test/stub changes to match upstream behavior

- remove admin panel visibility utility and non-standard test spy alias

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MenuService test stub method signatures

Align showMenu/hideMenu stub signatures with optional MenuID parameter so admin sidebar specs compile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* UoE/datashare: Fix home-page reload flicker (hydration-safe SSR anti-flicker overlay)

Reported: the page visibly flickers on reload. Root cause: provideClientHydration() is
enabled, but DSpace's ThemedComponent (header/navbar, footer, ...) builds its real content
imperatively via ViewContainerRef.createComponent() inside a client-side, async
ngAfterViewInit. Angular hydration cannot reuse an imperatively-created component, so that
subtree is destroyed and re-created client-side, visibly, after the rest of the page has
already painted (e.g. the navbar pops in ~500ms after the banner/search box, pushing all
content down). Same class of bug as upstream DSpace#3867.

Fix, ported from customer/mendelu's Angular-18 hydration-safe variant of the same fix already
shipped for other customer instances on this org's Angular-15/DSpace-7 branches:
- src/index.html: inline bootstrap script clones (never moves - a move would break hydration
  on this Angular-17 app) the SSR-painted <ds-app> into a detached, opaque overlay before
  Angular's bundle runs. The overlay sits on top while hydration + the imperative themed
  re-render happen invisibly underneath on the untouched original, and self-disables for
  Cypress/WebDriver-flagged automation.
- src/app/app.component.ts: removeSsrOverlayWhenContentVisible() drops the overlay once the
  auth/theme loader gate has opened AND the live <ds-app> DOM has settled (no element
  added/removed for 600ms, with real content present) - not on ApplicationRef.isStable, which
  stays busy long after the page is visually done. 15s hard fallback lives in index.html.
- src/typings.d.ts: Window.__dspaceRemoveSsrOverlay global the two files above share.
- src/app/app.component.spec.ts: regression test for the gate+settle removal logic.

Reproduced and verified locally in Docker (Dockerfile.dist SSR build) against a local DSpace
8.x REST backend - see flicker-evidence/README.md for methodology and before/after screenshots.

* test(admin-sidebar): run ngOnInit once per authz test, guard take(1) regression

Restructure the admin-sidebar authorization specs so each test configures its
stubs/spies and then triggers a single ngOnInit via a fresh component, removing
the manual comp.ngOnInit() that ran after the shared detectChanges(). Addresses
the duplicate-subscription review feedback (Copilot) on the three tests.

Add a regression test that drives isAuthenticated() as a BehaviorSubject
(false -> true) and asserts the admin menu is hidden then shown, so a
re-introduced take(1)/first() on the auth gate would fail the suite.

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

* feat: include file name in submission upload notifications

Multi-file drag-and-drop uploads showed generic 'Upload successful'/'Upload failed' toasts, so users could not tell which file each notification referred to. Add a non-breaking onCompleteItemWithFile output on UploaderComponent that carries the client-side file name alongside the parsed response, and use it (plus the existing error payload) in SubmissionUploadFilesComponent to render per-file notifications, falling back to the generic keys when no name is available.

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

* fix: make UploaderCompleteEvent fileName optional to match emitter

The emitter can pass undefined when the FileItem carries no file name; the interface claimed it was always present. Also cover the missing-file-name emit in the uploader spec.

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

* refactor: address Copilot review on upload notification typing

Omit fileName from UploaderCompleteEvent when unknown instead of emitting an explicit undefined, type the onUploadError output as EventEmitter<UploaderError>, and drop the explicit undefined fileName from the fallback spec.

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

* fix: prevent duplicate values in repeatable submission dropdowns

Selecting a value already present in another row of the same metadata field (e.g. the Funder dropdown) is now blocked: the change is reverted to the previous value and a warning notification is shown. Guard is scoped to scrollable dropdown models only.

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

* fix(statistics): clear statistics table pagination params on destroy

Statistics pagination is URL-driven with queryParamsHandling: 'merge', so a page/rpp selection made on one scope's statistics page persisted in the URL when navigating to another scope. Clear the table's own params on destroy, like other paginated components do.

Verified on the seeded local instance that ds-pagination already renders and pages correctly at community, collection and item level via the shared StatisticsTableComponent (DSpace#726); no further frontend change is needed for lower-level pagination.

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

* docs(statistics): explain deferred pagination-param cleanup accurately

The stale stats-* params only survive 'merge' navigations (e.g. the navbar search form); plain navigations drop them. Clarify that clearPagination stages nulls applied on the service's next updateRoute, and why no navigation is issued from ngOnDestroy (racing the in-flight navigation).

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

* test(statistics): destroy uninitialised table via the framework path

Let Angular invoke ngOnDestroy through fixture.destroy() on a component whose ngOnInit never ran (createComponent without detectChanges), instead of calling the hook directly.

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

* feat(submission): disable already-used options in repeatable dropdowns

Gray out (dsBtnDisabled + gray-200 background) and block options already
selected in another row of the same repeatable field, so duplicates cannot
be picked at all - mouse or keyboard. Also fix the duplicate-guard revert
leaving the rejected value on screen: setCurrentValue before dispatchUpdate,
no distinctUntilChanged on the control subscription, explicit model.value
revert in the guard.

Part of dataquest-dev/dspace-customers#624 (items 001, 005).

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

* style(submission): force disabled-option styling and normal hover cursor

Add !important to the disabled dropdown-option background/color so they win
over Bootstrap's .dropdown-item.disabled, and re-enable pointer-events with a
default cursor so hovering a used option shows a normal cursor (selection stays
blocked by the dsBtnDisabled/onSelect guards).

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

* fix(submission): refresh used sibling values on keyboard-open

selectOnKeyDown opened the dropdown with sdRef.open() directly on Enter,
bypassing openDropdown() so usedSiblingValues (and the option reload/page
reset) were stale -> duplicates could be selected via the keyboard-open
path. Route through openDropdown() instead. Addresses Copilot review on PR DSpace#26.

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

* refactor(submission): drop superseded duplicate-value guard, keep disabled-option prevention

* removed emptyline

* UoE flicker: block pointer-events on the frozen clone (Copilot); keep clone ids

Incorporates the applicable Copilot review point from the sibling PR DSpace#16 onto this
hydration-safe clone overlay, and records the reasoned outcome of the second one.

- pointer-events: the full-viewport SSR freeze-frame now intercepts pointer input
  (pointer-events: auto) instead of letting clicks pass through to the still-hydrating
  live app the user cannot see; flipped to none the instant fade-out starts so the
  revealed app is interactive for the whole 150ms crossfade.

- id-stripping (Copilot's other suggestion) is intentionally NOT applied: stripping ids
  from the clone unstyles every id-gated rule on it (global #main-content flex; header /
  #main-navbar under emulated encapsulation), which measured 40.65% clone-vs-final
  mismatch locally and turned the fade-out into the very jump the overlay removes. The
  clone keeps all ids and stays pixel-identical (0.4% removal diff); duplicate ids are
  harmless because the clone follows <ds-app> in document order. Rationale in the code.

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

* UoE flicker: add local Docker reproduction + fix-verification evidence

Independent, fully local repro (stock DSpace 8 backend on a clean DB in Docker + this
branch's production SSR build) with high-fps CDP screencast capture. Quantifies the
navbar-band deviation from the settled page:
  before (overlay off): navbar entirely missing mid-reload, up to 73.9% deviation, ~2.17s
  after  (overlay on):  frozen clone holds the full navbar, <=0.4% deviation, 0ms flicker
Includes before/after/diff screenshots, methodology, and the id-stripping measurement.

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

* Drop committed flicker screenshots from the PR — keep it source-only

Binary before/after screenshots don't belong in the source tree. The reproduction
methodology and the quantified before/after numbers live in the PR discussion instead.
Net PR diff is now source-only (index.html, app.component.ts, app.component.spec.ts,
typings.d.ts).

* UoE flicker: also mask the post-login white flash (unify overlay via fullscreen-loader watch)

PR DSpace#23 masked the home-page RELOAD flicker but not the POST-LOGIN flash: login hard-
redirects through /reload/<ts> -> /home and, while auth is re-established,
root.component.html hides the whole .outer-wrapper (incl. #main-content) via `d-none` and
paints .ds-full-screen-loader (white screen + spinner) -- on the current page right after
submit AND across the reload. The old overlay was gone by then, so the user saw a ~1s
white flash + content shift (admin sidebar). Verified locally in Docker (before: ~1.5s
white; after: eliminated).

This rewrites the inline bootstrap into one self-contained mask lifecycle that handles both:
- installMask(): freeze a detached CLONE of the last real SSR/routed view (still never moves
  the DOM Angular hydrates; still keeps all ids so the frozen frame is pixel-identical).
- A session-long MutationObserver re-masks with the last remembered real frame whenever the
  fullscreen auth/theme loader appears unmasked (covers login/logout, not just first load).
- revealWhenPainted(): lift (150ms fade) only once the routed page is genuinely painted --
  no .ds-full-screen-loader, #main-content AND the themed navbar have real layout height --
  and the DOM has been quiet for 800ms. The navbar/settle gate is what stops an intermittent
  reload FOUC (revealing over a half-built/unstyled page). 15s hard cap per mask.
- window.__dspaceRemoveSsrOverlay now just nudges the reveal, so AppComponent's existing
  DOM-settle hook (unchanged) still works; self-disables under Cypress/webdriver as before.

Verification: 12/12 logins and 8/8 reloads with zero blank/FOUC frames locally (only the
browser's own ~1-frame inter-document reload gap remains, which no in-page script can mask);
independently reviewed by 3 UX passes over before/after filmstrips -> all APPROVED.

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

* UoE flicker: render the desktop header on the server (fix reload content-shift)

The home-page reload showed a content-shift independent of login state: the
header's "Log In" control changed from a plain link to the dropdown button and
the search box resized on every load. Root cause: <ds-auth-nav-menu> (and other
responsive components) branch on HostWindowService.isMobile(), whose width
observable stayed empty during SSR because hostWindow.reducer's initial width was
null (HostWindowService filters out null widths). So the server rendered the
mobile branch and the client, once it measured the real desktop width, swapped to
the desktop branch — shifting the header and resizing search.

Fix: default the hostWindow width/height to a desktop viewport so SSR renders the
desktop layout that matches the common (desktop) hydration; the browser overrides
it with the real size immediately (StoreEffects.resize / AppComponent). Also guard
AppComponent's window-size dispatch to the browser so the server no longer
overwrites that default with an undefined size.

With this the SSR markup matches the settled client page, so the anti-flicker
overlay's frozen clone is pixel-identical to the revealed page (verified: reload
0/8 blank + 0/8 shift, clone==final, on the datashare theme).

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

* UoE flicker: update hostWindowReducer spec for the desktop default width

The reducer's initial state now defaults to a desktop viewport (1200x800) instead
of null so SSR renders the desktop layout (see prior commit). Update the spec that
asserted the null initial state accordingly.

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

* UoE flicker: mask logged-in reloads with the matching auth state (kill the sidebar shift)

A logged-in reload/refresh still shifted the whole body ~55px: the admin sidebar
is only rendered client-side after hydration, so it re-appeared over the masked
reload and the content reflowed at reveal. The overlay was masking with the
pre-hydration SSR clone, which is anonymous (no sidebar), so revealing it exposed
the shift.

Fix the mask source selection so it matches the auth state the page settles to:
- prefer the fresh SSR clone when it is already logged-in (the post-login /reload
  renders authenticated, sidebar included);
- else the last SETTLED remembered frame when THAT is logged-in (a plain logged-in
  F5 reload, whose SSR renders anonymous but whose remembered frame has the sidebar);
- else the remembered settled frame (or SSR clone on first load).
The logged-in header user-menu (.fa-user-circle / .dropdownLogout) is the tell.

Also remember the frame at reveal (once the live page has genuinely settled) rather
than from the unsettled pre-hydration SSR, so the remembered frame is always the
real final render.

Verified on the datashare theme (cpu1): logged-in reload 0/3 shift (was ~55px),
logged-out reload still 0/3, login no white flash and the sidebar no longer jumps.

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

* UoE flicker: drive the admin-sidebar gutter from CSS, not an animation

The page's offset beside the admin sidebar came entirely from a padding-left
applied by the @slideSidebarPadding animation: the sidebar is position: fixed,
so it contributes no layout width of its own. That animation reads its width
from the CSS-variable store, which is populated by scraping document.styleSheets
-- something platform-server does not have. The server therefore rendered
padding-left: 0 and the browser animated 0 -> 55px over 300ms, sliding the whole
page sideways on every authenticated reload.

Resolve the gutter from the --ds-admin-sidebar-* custom properties via CSS
classes instead, so it renders identically on the server and in the browser with
no hardcoded width, and keep the 300ms slide for genuine pin/unpin behind a
separate ds-admin-sidebar-animate class.

This mirrors the fix already carried on dataquest-dev/dspace-angular branch
customer/vsb-tuo, with one adaptation: that branch arms the transition one
requestAnimationFrame after first paint, which is enough on DSpace 7. DSpace 8
discards the server-rendered DOM and re-renders it ~350ms later, so the gutter
briefly falls back to 'hidden' well after that first paint; arming on the first
post-bootstrap 'visible' instead keeps that recovery instant.

Measured on a live instance with the anti-flicker overlay both on and off --
measuring only with it on validates the overlay, not the fix:
  authenticated F5  17 animated frames (0 -> 55px over 280ms)  ->  0
  anonymous F5      0.0px                                      ->  0.0px
  pin/unpin toggle  animates, 17 frames                        ->  unchanged

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

* UoE flicker: reserve the scrollbar gutter so the page stops sliding on reload

While Angular boots it replaces the server-rendered DOM, and for ~90ms the page
is too short to scroll. The vertical scrollbar disappears and comes back, so the
viewport widens from 1425 to 1440 and back, and every centred container slides
half the scrollbar width to the right and then back to the left.

The anti-flicker overlay cannot hide this one: the overlay lives inside the page,
so it shifts along with everything else. That is why the reload still visibly
jumped after the gutter/padding work -- it was never the admin sidebar.

Reserving the scrollbar's width permanently makes its appearance and
disappearance a no-op for layout.

Measured on the real /home page, authenticated, in a headed browser (headless
Chromium uses overlay scrollbars that occupy no width, so it cannot reproduce
this at all -- and an earlier attempt measured the wrong page entirely, because
login hard-redirects to the end-user agreement until that is accepted):

  full-frame horizontal shift, overlay on:  +8px at 758-811ms  ->  0px
  max frame difference vs the settled page:  8.1%              ->  0.1-0.3%

over three runs. The remaining sidebar-gutter flap during the component's
re-creation stays covered by the overlay, which is what the overlay is for.

Also nest a second animation frame before arming the gutter transition, so the
arming can never share a style recalculation with the class change it must not
animate. Defensive: the gutter applied instantly with a single frame in every
run measured here.

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

* UoE flicker: stop the overlay eating clicks, and drop the dead half of it

Three defects the earlier commits introduced or left behind:

The overlay set `pointer-events: auto` deliberately, reasoning that a click on a
frozen clone must not reach the live app behind it. In practice that made the
whole page unresponsive for as long as the mask was up: you click a nav link and
nothing happens, which reads as a broken site. It now never absorbs input -- the
click goes through to the live app -- and the mask lifts on the first interaction,
so you see the result of the click instead of a picture of the previous page.

`scrollbar-gutter: stable` collided with ng-bootstrap, which pads the body by the
scrollbar width when a modal opens to compensate for the scrollbar it hides. With
the gutter reserved permanently that width is never freed, so the compensation was
pure surplus and shifted the page 7.5px left on every modal -- in a change whose
purpose is removing layout shifts. Cancelled for `body.modal-open`.

The remembered frame was stored under one unkeyed slot, so a hard navigation or a
logout could paint the previous page -- including logged-in chrome -- over the new
one. It is now keyed by URL and only reused on a match.

Also removes ~230 lines that never did anything: AppComponent's overlay driver, its
spec and its Window type augmentation all existed to call
`window.__dspaceRemoveSsrOverlay`, which is `function () { revealWhenPainted(); }`
-- the same function index.html already re-schedules every frame until the page
settles. The two sets of tuning constants (600ms/10s vs 800ms/15s) could never take
effect. Deleted along with the global itself.

Verified on the running instance, headed, on the real /home page:
  incognito, click 300ms after load   click swallowed        -> navigates
  modal open                          content -7.5px         -> 0px
  authenticated F5                    0px                    -> 0px (unchanged)
  navigation between pages            0px                    -> 0px
  cold login (home never visited)     no white flash         -> no white flash

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

* feat(submission): say when a file is being ingested instead of just "Saving"

An ingest from a server path happens inside the save request, so for a large
dataset the submission form sat on a bare "Saving..." for minutes with
nothing to say a transfer had started or whether the tab could be closed.

While a save carries a pending local.bitstream.redirectToURL value the
progress bar now reads "Ingesting file from server..." with a running
mm:ss counter, alongside a note that the file is being read directly from
the server and the tab should stay open. Ordinary saves are unchanged.

The elapsed time is measured from wall-clock timestamps rather than by
counting timer ticks, because browsers throttle timers in a backgrounded tab
- exactly where a long ingest is left running.

The pending state is latched when the save is dispatched, so the label
describes the request actually in flight rather than a form that has since
changed, and a pending removal of the field wins over a value still sitting
in the section data, so clearing the field is not reported as an ingest.

"Saving..." and "Depositing..." were hard-coded English in the template and
are now translated like everything else.

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

* UoE flicker: move the overlay script out of index.html into a source file

The anti-flicker overlay has to run as a synchronous inline script before the
deferred module bundles, so it cannot live inside the Angular app. But it does not
have to be pasted into index.html as a 200-line blob. Keep the source in
src/anti-flicker-overlay.js -- a normal, formatted, editable file -- and inline it
into index.html at build time via the build's indexTransform hook
(webpack/index-html-transform.ts), which the custom-webpack builder already supports.

The built index.html is unchanged: the script is still inlined, still ahead of the
module bundles, same runtime behaviour. Only the source is cleaner -- index.html goes
from 226 lines to 38, and the overlay logic reads like code instead of markup. The
transform throws if the marker is missing, so the overlay can never silently drop out
of a build.

Verified after the change, on the running instance:
  built dist/index.html          overlay inlined ahead of the module scripts, marker gone
  SSR-served /home               overlay present inline
  incognito, click 300ms in      navigates (click passes through)
  authenticated F5, headed        0px

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

* fix(submission): remove obsolete "same file name" upload warning and block

Two files with the same name are handled correctly in the DSpace 8.3 backend
(the DataShare dataset zip disambiguates duplicate entry names via
uniqueEntryName(), and per-file download is UUID-addressed), so the submission
UI no longer needs to warn about - or block deposit on - duplicate file names.

Removes the DataShare duplicate-file-name detection end to end while keeping the
separate total-upload-size feature untouched:

- section-upload.component: drop the duplicate warning message + duplicate
  detector wiring; keep the total-size calculation and its warning
- submission-form-footer.component: restore the vanilla deposit() (no longer
  gated by the duplicate check / "cannot submit" notification)
- datashare-submission.service: remove duplicate-detection helpers, the shared
  hasUploadFilesErrors signal and sendCannotSubmitNotification; keep the
  file-size helpers
- i18n (en + datashare theme): remove the now-unused
  datashare.submission.sections.upload.duplicates and .submit.errors keys
- specs: drop the removed mocks/providers

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

* fix(submission): remove obsolete "same file name" upload warning and block

Two files with the same name are handled correctly in the DSpace 8.3 backend
(the DataShare dataset zip disambiguates duplicate entry names via
uniqueEntryName(), and per-file download is UUID-addressed), so the submission
UI no longer needs to warn about - or block deposit on - duplicate file names.

Removes the DataShare duplicate-file-name detection end to end while keeping the
separate total-upload-size feature untouched:

- section-upload.component: drop the duplicate warning message + duplicate
  detector wiring; keep the total-size calculation and its warning
- submission-form-footer.component: restore the vanilla deposit() (no longer
  gated by the duplicate check / "cannot submit" notification)
- datashare-submission.service: remove duplicate-detection helpers, the shared
  hasUploadFilesErrors signal and sendCannotSubmitNotification; keep the
  file-size helpers
- i18n (en + datashare theme): remove the now-unused
  datashare.submission.sections.upload.duplicates and .submit.errors keys
- specs: drop the removed mocks/providers

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

* UoE flicker: minify the overlay when inlining it into index.html

The overlay must be inlined (it runs before the deferred bundles), so it always
appears in the served HTML — "view source" showed 200 readable lines. Minify it
with terser at inline time: the served page now carries a single ~2.7kB line with
a one-line banner pointing back to the source. src/anti-flicker-overlay.js stays
the readable, formatted source; only the browser copy is compacted. Runtime
behaviour is unchanged.

Verified: served /home inline script is 2 lines (banner + minified body) instead
of ~200; the overlay logic is intact (mask installs, click passes through, reveal
works).

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

* fix(item-page): stop 500 on "show more" for items with many bitstreams

The datashare theme already renders CC-LICENSE/LICENSE in its own Licences
section pinned to page 1. The base FileSectionComponent also fetched CC-LICENSE
in lockstep with ORIGINAL on every "show more" click, using the same page
number while isLastPage was derived from ORIGINAL only. Once ORIGINAL had more
pages than CC-LICENSE, later clicks requested a CC-LICENSE page past its end,
which the REST API answered with HTTP 500 - and it also rendered the licence
file twice. Fetch ORIGINAL only here.

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

* refactor(datashare-submission): address Copilot review on total-size helpers

- MAX_FILE_SIZE_GB/BYTES -> private readonly (used only inside the service; they are constants)
- formatBytes: add TB/PB units and clamp the unit index so values >= 1 TB no longer render "undefined"

Behaviour for in-range values (<= 20 GB upload cap) is unchanged; verified across ranges.

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

* refactor(item-page): revert getNextPage error handling to upstream

Addresses Copilot review: clearing isLoading on the error branch enabled a
retry that requested the next page rather than re-requesting the failed one,
because currentPage is incremented up front. That error-handling tweak was not
needed for the 500 fix, so getNextPage now matches upstream exactly (isLoading
cleared only on success) - the fix is solely dropping the CC-LICENSE fetch.

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

* UoE flicker: slide the admin sidebar in on login (content stays put)

A genuine login now animates the admin sidebar into view. The auth effect flags
the login (one-shot, never on a plain reload); the anti-flicker overlay, as it
lifts on the /home the login lands on, adds a body class that runs a CSS keyframe
sliding the sidebar in from the left.

Crucially this is NOT the vanilla gutter animation, which slides the page content
55px — that is the same shift removed from the reload path, and bringing it back
would reintroduce exactly that. Here only the fixed sidebar's transform animates;
the content and its reserved gutter never move. So the entrance is visible on login
while reload stays perfectly still.

The flag is consumed only on a reveal that sticks (no re-mask within 250ms): login
redirects through /reload -> /home with several mask/reveal cycles, and an earlier
transient reveal must not eat the one-shot flag before the final, visible one.
Respects prefers-reduced-motion.

Measured, headed, on the real pages:
  login            sidebar slides -55 -> 0, visible after the mask lifts; content 0px (3/3 runs)
  authenticated F5 content 0px, no entrance animation (flag absent on reload)
  anonymous F5     0px
  modal open       content 0px
  click through mask navigates

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

* docs(item-page): document base-vs-theme licence rendering; pin spec

Expert review follow-ups:
- Class doc on FileSectionComponent stating it lists ORIGINAL only and that
  licence bundles (CC-LICENSE/LICENSE) are rendered by the theme, so the base
  never fetches them (paging them in lockstep past their end caused the 500).
- Spec now asserts findAllByItemAndBundleName is called with ORIGINAL and never
  with CC-LICENSE/LICENSE, so a regression re-introducing the second fetch fails.

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

* Revert "UoE flicker: slide the admin sidebar in on login (content stays put)"

This reverts commit 6ad726e.

* fix(submission): show an actionable error when a server-path ingest fails

Entering a path that does not exist (or a URL, or a path outside the allowed
directories) failed the save with the generic "There was an issue when saving
the item, please try again later." - which gives the administrator nothing to
act on, since the real cause is the path they typed.

The backend returns a 400, but DSpace genericises the message and the save
effect discards the error, so the specific reason cannot reach the UI. Instead,
the saveError effect now checks whether the failed save carried a pending
local.bitstream.redirectToURL value - the rolled-back operations still hold it
at that point - and, if so, shows a message that says what to check: an
absolute path, to a file that exists on the server, inside an allowed
directory, and not a URL. Every other save error keeps the original notice.

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

* fix(submission): shorten the upload-from-path error to a single plain sentence

The earlier message spelled out every possible cause; a plain 'the file could
not be found at the path you entered' is enough.

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

* test(item-page): isolate ORIGINAL-only assertion to the click

Copilot review: detectChanges() runs ngOnInit -> getNextPage, so the spy is
already called before the click. Reset the spy first so the assertions verify
the click behaviour, not ngOnInit.

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

* Merge remote-tracking branch 'origin/datashare-UoEMainLibrary-dspace-8_x' into uoe/bitstream-upload-from-path

* test(item-page): assert requested bundle names directly

Copilot review: jasmine.anything() does not match undefined (comp.item is
unset), weakening the negative assertions. Capture the bundle-name argument of
every call and assert it equals exactly ['ORIGINAL'] - independent of the item
argument, and failing if any licence bundle is fetched.

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

* refactor(datashare): type the tri-state path outcome as boolean | undefined

Address Copilot review: pendingOutcome and pathOutcome genuinely return a
third state (undefined = no pending operation touches the field) alongside
true/false, so declaring them boolean masked that. Typing them honestly and
narrowing with a type guard makes the undefined branch explicit.

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

* docs(item-page): fix misleading bitstream pageSize comment

The old comment claimed 'override to 5 (default) as per config.prod.yml',
but the code sets pageSize = 25 and no config.prod.yml sets
item.bitstream.pageSize (its default is 5). Reword to state the value is
hard-coded to 25 on purpose to load more files per 'Show more' click.

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

* UoE flicker: declare terser as an explicit devDependency

webpack/index-html-transform.ts imports terser directly (to minify the inlined
anti-flicker overlay), but terser was only present transitively via the Angular
build tooling. Declare it explicitly so the build does not rely on hoisting.

Pinned to 5.29.1 to match the existing yarn.lock resolution; `yarn install
--frozen-lockfile` passes with no lockfile change. Addresses a Copilot review note.

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

* chore(submission): shorten the datashare comments

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

* fix(comcol): widen collection/community logo max-width so wide banners aren't squeezed

The upstream `--ds-comcol-logo-max-width: 500px` cap limits the logo's
*width*. Wide banner-style logos are therefore squeezed into a thin strip:
e.g. the RESPIRE collection logo (6000x1100 PNG, 5.45:1) renders at only
~500x92, which users report as a "tiny logo".

Raise the width cap to 100% so wide logos fill the header/content column.
The `--ds-comcol-logo-max-height: 500px` cap is kept, so tall/square logos
are still bounded and only genuinely wide banners get larger.

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

* fix(comcol): enlarge result thumbnail width to 175px

Result-list/detail thumbnail width is capped by --ds-thumbnail-max-width
(125px); customer reports thumbnails look small. Raise to 175px, which
matches the backend-generated thumbnail resolution (thumbnail.maxwidth = 175),
so it stays crisp with no backend change / thumbnail regeneration needed.
Height (--ds-card-thumbnail-height) is intentionally left at 240px.

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

* UoE/feat(mydspace): rename submission menu and status labels (LUC070-260)

Renames the /mydspace "Show" options, the page headings beside them and
the Status filter values, and fixes the applied filter showing a raw
Solr token instead of a readable label.

- "Show" dropdown: "Submissions" -> "My submissions",
  "Workflow tasks" -> "Submissions – in review"
- Status facet: "Workspace" -> "In draft", "Workflow" -> "In review",
  "Validation" -> "For validation". "Archived" and "Waiting for
  reviewer" keep their names, as the ticket asks
- Result-card badges and the Administer Workflow screen follow the same
  wording, so a row cannot say "In draft" in the filter and "Workspace"
  on its badge
- Avatar menu, breadcrumb, page title and "Back to Submissions" move to
  "My submissions" together, so the path to the page reads consistently

Ticking a Status value used to relabel the checkbox and the applied
filter chip with the raw token ("item" for Archived,
"waitingforcontroller" for Waiting for reviewer). The unticked facet
list keys off FacetValue.value, which cerialize aliases onto the backend
label; the ticked option and the chip key off AppliedFilter, which
carries the Solr token untouched. Adding the lower-cased token aliases
makes all three render the same label, with no component change.

The screen-reader announcement and the chip aria-label passed the same
raw token through; both now resolve it through the key the visible text
already uses.

Also supersedes the December 2025 naming scheme left behind in the
DataShare theme file. It mapped workspace -> "In review" and workflow ->
"Draft" (the wrong way round), was reverted from src/assets/i18n/en.json5
in 457b86a, and declared two keys twice. It is inert today because
merge-i18n is wired into no build step, but one manual run would have
reinstated it over this change.

"Supervised items" is deliberately left untouched, pending the customer
decision on what it should be called or whether to hide it.

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

* fix(submission): harden duplicate detection in repeatable scrollable dropdowns

- key duplicate detection on authority (fallback value) so authority-controlled vocabularies (e.g. Funder) no longer accept the same entry twice

- re-check live sibling values at commit time (selectEntry) to close the stale-set gap when a value is chosen in another row after opening

- block mousedown on disabled options (dsBtnDisabled); options commit on mousedown

- guard keyboard Enter against selecting while options are still loading

- expose aria-selected on options

- add reproduction/regression spec and a self-contained before/after demo

* style: satisfy lint (indentation + import sort)

* a11y: bind aria-selected to the selected value; clarify 1e spec name (review)

* fix(i18n): restore keys dropped by the keep-embargo cherry-pick

4b21906 resolved the en.json5 conflict by taking the source branch's
whole file. That branch (uoe/move-keep-embargo-policies, cut 2026-06-30)
predates three keys the customer already had from sync PR #6, so the
cherry-pick silently deleted them:

  form.vocabulary.load-error
  item.page.filesection.checksum
  item.page.doi.pending

All three are referenced by code shipped in this PR, and
MissingTranslationHelper renders the raw key, so the UI would show
"item.page.filesection.checksum" twice per bitstream on the full item
record and "item.page.doi.pending" on items awaiting DOI registration.
The datashare theme copy does not help: angular.json never copies
src/themes/*/assets/i18n into the build and merge-i18n is a manual
script, so src/assets/i18n/en.json5 is authoritative at runtime.

Unit tests cannot catch this - TranslateLoaderMock returns {}, so the
specs assert on the raw key and pass either way.

en.json5 is taken whole from the source branch; the two-dot diff on that
path is additions-only, so this restores the three keys and changes
nothing else.

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

* chore: keep local-only test Dockerfile and demo harness out of the PR

Cherry-pick of dataquest-dev/uoe-dspace-datashare-angular 85124a0,
authored 17 minutes after the last commit picked into this sync PR and
so left outside the pick window. It is the only non-merge commit from
the sync window that did not make it across.

2bc0bd6 faithfully carried Dockerfile.karma and demo/** over together
with the dropdown fix; upstream then removed them again as local-only
recording evidence. Without this commit the sync ships 13 files /
1,574,221 bytes - 98.7% of it incompressible .webm/.gif/.png - and git
history is append-only, so a later deletion would not reclaim it.

Dockerfile.karma also sits at the repo root with no CMD/ENTRYPOINT and an
unpinned node:20-bookworm base, and demo/package.json is a second,
unmanaged manifest that dependency scanners would pick up.

Note: this keeps the blobs out of the customer's history only under a
squash merge, as every previous sync PR was merged. Under a plain merge
commit or a rebase merge, 2bc0bd6 still introduces them.

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

* fix(submission): stop repeatable dropdowns duplicating and clearing values

Follow-up to DSpace#26 and DSpace#32. Both reported problems still reproduced on
datashare-UoEMainLibrary-dspace-8_x, because they are not caused by the dropdown
widget alone.

Deleting a row silently rebound the surviving rows to the wrong controls
------------------------------------------------------------------------
getControlOfGroup() stamped a `startingIndex` on a group model the first time the
row rendered and resolved that row's FormGroup as control.get([startingIndex])
from then on. Nothing re-synced it, while DynamicFormArrayModel re-indexes its
groups on every insert/remove/move and the template binds formGroupName to that
live index. After deleting a non-last row the two disagreed: surviving rows
resolved to another row's control or to null, and after delete+add two rows could
alias onto the same control. That is what let a value the user never touched be
overwritten, and what made the duplicate check read stale sibling values.

The live index is now the single authority. Drag and keyboard reordering moved
the group models only, which is what the frozen index had been compensating for,
so both now move the control alongside the model (moveGroupAndControl).

DsDynamicFormControlContainerComponent.ngOnChanges also dereferenced a null
`group` in the tick after a row was removed, throwing inside change detection and
aborting the pass for the rest of the field.

A click aimed at dismissing the menu erased the value
-----------------------------------------------------
The menu is a full-width overlay drawn over the field's own bottom edge and the
rows beneath it, and its first entry was the destructive "Clear selection" - so
the spot a user naturally clicks to dismiss the dropdown wiped their selection,
row after row. Clearing now sits at the end of the menu, visually separated, and
the menu keeps a small gap below the input. The clear entry also carried both
(click) and (mousedown) and therefore fired twice per interaction; it now has a
single handler, like the options.

Options deliberately keep committing on (mousedown): blurring the input flips
showErrorMessages on a required field and forceShowErrorDetection() destroys and
re-creates the control, so the element is gone before a click event could reach
it. This is now documented in the template.

Duplicate detection
-------------------
usedSiblingValues was a snapshot refreshed only in openDropdown()/selectEntry(),
so it went stale whenever a row was added, removed or edited - and the toggle
caret opens the menu through NgbDropdown without calling openDropdown() at all,
leaving the set empty and nothing greyed out. It is now derived from the live
models, and every path that opens the menu refreshes the options via
(openChange).

DSpace#32's canonicalKey() keyed on `authority ?? value`, but the same entry arrives as
a VocabularyEntry when picked in-session and as a FormFieldMetadataValueObject
when rebuilt from stored metadata, and only one side may carry an authority. The
keys were then incomparable and the duplicate went through. Identity is now the
authority *and* the normalised value, and a match on either is a duplicate.

Testing
-------
14 specs written first against the unfixed code and confirmed failing, covering
the row/control binding across remove/insert/reorder and every duplicate and
clearing path. Full suite: 5478 passing.

Verified end to end on a dockerised DSpace 8 backend with the datashare theme:
the three Type rows survive the dismissing click, the still-used values are
greyed out after delete+add (input and caret alike), and what the server stores
matches the form with no duplicate and nothing destroyed.

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

* fix(submission): keep keyboard reorder-cancel and clear-entry consistent

Both from the Copilot review of this PR; both are regressions this PR introduced.

cancelKeyboardDragAndDrop() still moved the group models only, so Escape after a
keyboard reorder restored the model order while leaving the FormArray as it was -
the exact desync this PR set out to remove. It now goes through
moveGroupAndControl() like the drag and arrow-key paths.

Dropping the clear entry's (click) handler to stop it firing twice per mouse
interaction also removed its keyboard activation, since a native button turns
Enter into a click. It now carries (keydown.enter) alongside (mousedown), which
mirrors how the options are bound.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Juraj Roka <95219754+jr-rk@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: MatusBeke <matus.beke7@gmail.com>
Co-authored-by: milanmajchrak <minptai7@gmail.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.

3 participants