Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions RELEASE_NOTES/release-4.2.11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Release 4.2.11 — 2026-08-03

| | |
| ---------------------------- | --------------------------------------------- |
| **Build branch deployed** | `release-4.2.11` (Jenkins deploy source) |
| **Tag** | `v4.2.11` (immutable marker + GitHub Release) |
| **Baseline (previous prod)** | `v4.2.10` (2026-08-03) |
| **Commits** | `1` |
| **Author** | Likhith Thammegowda |

## Summary

The public course overview page showed the internal creator username as the author —
`creatorjhpaastrika_0qfj` rather than `Jhpiego Cooperation`. It now shows the same author
name the signed-in course page shows.

## 🐛 Fixes

- **public-toc** — the author line now resolves the display name from `creatorDetails`
instead of rendering the raw `creator` username. The signed-in TOC reads
`creatorDetails[0].name`, but that only works there because `app-toc-home` parses the field
first; the public page is fed by the public search API, which returns `creatorDetails` as a
JSON **string**, so indexing it directly yields `"["` and then `undefined`. The value is now
resolved from either shape — string or already-parsed array — and falls back to `creator`
when the details are malformed or absent, so it can never render less than before
(`a4604b993`)

## 🏗️ Build/CI

- None.

## 📚 Docs/Chore

- None.

## ⚠️ Deploy notes & risk

- **Config / env / secret changes:** none
- **Backend / API contract dependencies:** none. This is a client-side display change; the
search payload is unchanged
- **Breaking changes:** none
- **Risk note:** low. The change is confined to the public course overview banner and is
fallback-guarded, so the worst case is the previous behaviour (the username). Confirmed
working on the page before release, and covered by 7 unit tests spanning both payload
shapes plus the malformed, absent, no-name and null-content paths

## ✅ Pre-deploy checklist

- [x] Node 20 active (`nvs use 20`)
- [x] Build verified (`yarn run build:local`)
- [ ] `yarn run lint` clean — pre-existing repo-wide `@typescript-eslint/ban-types`
rule-not-found error blocks a clean lint run (known issue, see CLAUDE.md); no new lint
errors introduced by this release
- [x] Unit tests green (`yarn test`) — `public-toc-banner.component.spec.ts` 13/13, including
7 new cases for the author name resolution
- [x] Verified on the running app — the public overview now shows `Jhpiego Cooperation`
- [ ] Rollback ref confirmed (re-runnable in Jenkins): `release-4.2.10`

## Release & rollback

**Deploy** — a human runs the manual Jenkins job pointed at the **build branch**
`release-4.2.11` (deploy is from a branch, not a tag). Each release gets its own new build
branch + a `v4.2.11` tag; the previous `release-4.2.10` branch stays frozen.

**Rollback** — re-run the same manual Jenkins job against the previous release branch
`release-4.2.10`.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<span>{{ 'Last updated on' | translate }}</span> : {{ content?.lastUpdatedOn | date }}
</div>
<div class="details"><span>{{ 'Source' | translate }}</span> : {{content?.sourceName}}</div>
<div class="details" *ngIf="content?.creator"><span>{{ 'Author' | translate }}</span> : {{content?.creator}}</div>
<div class="details" *ngIf="authorName"><span>{{ 'Author' | translate }}</span> : {{ authorName }}</div>
</div>
<div class="lable1" [ngClass]="{'ws-mat-primary-text link-active': routelinK === 'license'}" wsUtilsRetain>{{ 'View
License Terms' | translate }}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,43 @@ describe('PublicTocBannerComponent', () => {
component.createAcct()
expect(routerSpy).toHaveBeenCalledWith('app/create-account')
})

describe('authorName', () => {
const details = [{ id: '28ec6b71', name: 'Jhpiego Cooperation' }]

it('parses creatorDetails when the search API returns it as a JSON string', () => {
component.content = { creatorDetails: JSON.stringify(details), creator: 'creatorjhpaastrika_0qfj' }
expect(component.authorName).toBe('Jhpiego Cooperation')
})

it('reads creatorDetails when the content service returns it already parsed', () => {
component.content = { creatorDetails: details, creator: 'creatorjhpaastrika_0qfj' }
expect(component.authorName).toBe('Jhpiego Cooperation')
})

it('falls back to creator when creatorDetails is malformed', () => {
component.content = { creatorDetails: '[{oops', creator: 'creatorjhpaastrika_0qfj' }
expect(component.authorName).toBe('creatorjhpaastrika_0qfj')
})

it('falls back to creator when creatorDetails is absent', () => {
component.content = { creator: 'creatorjhpaastrika_0qfj' }
expect(component.authorName).toBe('creatorjhpaastrika_0qfj')
})

it('falls back to creator when creatorDetails carries no name', () => {
component.content = { creatorDetails: [{ id: '28ec6b71' }], creator: 'creatorjhpaastrika_0qfj' }
expect(component.authorName).toBe('creatorjhpaastrika_0qfj')
})

it('returns an empty string when there is nothing to show', () => {
component.content = {}
expect(component.authorName).toBe('')
})

it('does not throw when content is null', () => {
component.content = null
expect(component.authorName).toBe('')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,29 @@ export class PublicTocBannerComponent implements OnInit {
tocConfig: any = null
routelinK = 'license'
displayStyle = 'none'

/**
* Display name for the author, matching what the authenticated TOC shows.
*
* `creator` is the raw username (e.g. `creatorjhpaastrika_0qfj`); the human name lives in
* `creatorDetails[0].name`. This page is fed by the public search API, which returns
* `creatorDetails` as a JSON **string**, while the content-service returns it already
* parsed — so handle both rather than indexing blindly, which would yield `"["` and then
* `undefined`. Falls back to `creator` so this can never render less than it does today.
*/
get authorName(): string {
const raw = this.content ? this.content.creatorDetails : null
let details: any = raw
if (typeof raw === 'string') {
try {
details = JSON.parse(raw)
} catch {
details = null
}
}
const first = Array.isArray(details) ? details[0] : details
return (first && first.name) || (this.content ? this.content.creator : '') || ''
}
constructor(
private http: HttpClient,
private signUpSvc: SignupService,
Expand Down
Loading