Skip to content

fix(edit-content): normalize string lockedBy shape for Pages in lock warning#35726

Open
oidacra wants to merge 3 commits into
mainfrom
issue-34541
Open

fix(edit-content): normalize string lockedBy shape for Pages in lock warning#35726
oidacra wants to merge 3 commits into
mainfrom
issue-34541

Conversation

@oidacra
Copy link
Copy Markdown
Member

@oidacra oidacra commented May 15, 2026

Summary

  • Re-fix for [DEFECT] Locked content shows undefined username in New Edit Content mode #34541 after QA found a regression on Pages in new Edit Content: the lock banner showed "Content locked by ." and also appeared when the current user had locked the Page.
  • Root cause: the backend returns lockedBy in two shapes — DefaultTransformStrategy produces an object { userId, firstName, lastName }, but PageViewStrategy overwrites it with the raw userId string plus a separate lockedByName. The frontend lock warning assumed only the object shape.
  • Fix is frontend-only: normalize both shapes inside lockWarningMessage (use lockedByName for display when lockedBy is a string, compare the string directly to currentUser.userId). Widen the model type to document the dual contract.

Why frontend-only

Roughly 37 frontend files (UVE, edit-ema, sdk) already treat Page's lockedBy as a string. Normalizing the backend would have a much broader blast radius and is out of scope for this QA fix.

Acceptance Criteria

Test Plan

  • yarn nx test edit-content --testPathPattern=lock.feature — all tests pass, 3 new tests added
  • yarn nx lint edit-content — clean
  • Manual: lock a Page as user A, open as user B → expect full name in banner
  • Manual: lock a Page as user A, reopen as user A → expect no banner
  • Manual: lock a non-Page contentlet as another user → expect firstName/lastName

Changed Files

File Change
core-web/libs/edit-content/src/lib/store/features/lock/lock.feature.ts Normalize string vs object lockedBy
core-web/libs/dotcms-models/src/lib/dot-contentlet.model.ts Widen lockedBy?: DotContentletLockUser | string, add lockedByName?: string
core-web/libs/edit-content/src/lib/store/features/lock/lock.feature.spec.ts 3 new tests for Page shape

Closes #34541

…warning

Pages return `lockedBy` as a userId string (with `lockedByName` for the full
name) while other content types return an object `{ userId, firstName,
lastName }`. The lock warning computed assumed the object shape, so for Pages
it could not detect "locked by current user" and rendered an empty name in the
banner ("Content locked by .").

Normalize both shapes when deriving the locker userId and display name, and
widen the model type to document the dual contract.

Closes #34541
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 15, 2026

Claude finished @oidacra's task in 2m 3s —— View job


Review — PR #35726

  • Read changed files (lock.feature.ts, model, spec)
  • Verify backend dual-shape claim
  • Analyze edge cases and risks
  • Post review

Verdict: looks clean. Backend dual-shape claim is verified (DefaultTransformStrategy.java:391 writes a map, PageViewStrategy.java:107-108 writes a string + separate lockedByName). Frontend normalization is correctly scoped and well-tested. A handful of small things to consider, none of them blockers.

Things worth a second look

1. Type widening is correct, but [key: string]: any undermines it as a safety netdot-contentlet.model.ts:27 now declares lockedBy?: DotContentletLockUser | string, which is right contract-wise. But DotCMSContentlet has [key: string]: any at line 48, so consumers who happen to bypass the declared field still get any. The runtime typeof lockedBy === 'string' check at lock.feature.ts:68 is doing all the real work — the union type is documentation, not enforcement. Fine, just worth being explicit about.

I verified no other call site in core-web reads DotCMSContentlet.lockedBy as an object (grep contentlet.lockedBy came back empty), so the widening doesn't break existing consumers.

2. The TODO at lock.feature.ts:67 has no tracking issue — "remove this branching once the backend normalizes the shape" is the right long-term plan, but without a linked issue this will rot. Either link to a follow-up backend ticket or drop the TODO (the comment block above it is enough to explain why).

3. Empty-object lockedBy = {} edge case is silently swallowed — if backend ever returns lockedBy: {} (no userId), the code computes lockerUserId = undefined, then currentUser?.userId === undefined only matches when currentUser has no userId either. userDisplay becomes '', so !userDisplay returns null and the banner is suppressed. This is benign (no broken UI) but means malformed payloads disappear silently. Not worth changing for this fix; just flagging.

4. Minor: the new logic could be a helper — the resolve-display-name dance (lines 68–74) is the load-bearing piece of lockWarningMessage and would read better as a small private function resolveLocker(lockedBy, lockedByName): { userId, display }. Optional polish.

Things checked and clean

  • lockerUserId comparison correctly handles both string and object shapes against currentUser.userId (string in both cases).
  • userDisplay.trim() plus the !userDisplay check correctly suppresses the "Content locked by ." rendering — confirmed by the new whitespace-only test at lock.feature.spec.ts:207.
  • The new no.permission fallback (line 88) is a real UX improvement over the prior code, which would have rendered "Content is locked by . You don't have permissions…" for nameless lockers.
  • Test coverage is solid: object shape, string shape, self-lock for both shapes, missing name, whitespace name, no-permission with/without name. Twelve tests for one computed, which is appropriate given the matrix.
  • DotPage-style consumers (37-file claim in PR body) use dot-page.model.ts where lockedBy: string is already correctly typed; this PR doesn't touch them, so blast radius is what the description says.

Acceptance criteria reality check

The "Manual: lock a Page as user A" rows in the PR test plan are still unchecked. Worth doing before merge — the unit tests prove the normalization logic but not that the banner actually renders the right copy end-to-end (e.g., that lockedByName actually arrives at withLock's store from the Page edit flow).
branch issue-34541

…d rephrase shape comment

Address code-review feedback on PR #35726:

- Add missing test for the no-permission branch when `lockedBy` is a string
  (Page content type) — the analogous object-shape case was already covered.
- Rephrase the shape comment so it describes the two API shapes generically
  instead of attributing the string shape to "Pages", which couples the
  comment to a backend categorization that could be normalised later.
…navailable

PageViewStrategy.getLockedByUserName returns null when loadUserById throws
DotSecurityException, which makes an empty `lockedByName` reachable in
production. The previous behavior interpolated the empty string into the i18n
template, rendering "Content locked by ." — the same regression originally
reported in #34541. Apply the fix consistently to both shapes:

- Trim the computed display name.
- If empty and the user lacks unlock permission, use the existing name-less
  `edit.content.locked.no.permission` key.
- If empty and the user can unlock, suppress the banner (the lock switch UI
  still indicates the locked state) instead of rendering a misleading message.

Update affected tests to assert the intended user-visible behavior rather than
the previous edge-case output, and add coverage for the no-permission +
no-name path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[DEFECT] Locked content shows undefined username in New Edit Content mode

1 participant