Skip to content

feat(series): display multiple series memberships#597

Merged
neonsolstice merged 1 commit into
bookorbit:mainfrom
chrismansell26:BO-595-display-multiple-series
Jul 9, 2026
Merged

feat(series): display multiple series memberships#597
neonsolstice merged 1 commit into
bookorbit:mainfrom
chrismansell26:BO-595-display-multiple-series

Conversation

@chrismansell26

@chrismansell26 chrismansell26 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Before you submit

New feature? Please open an issue first and get approval from the maintainers before writing any code.
PRs that introduce features without a prior discussion will be closed.

Used AI assistance? That's fine - but make sure you've read and understood every line of the diff
before submitting. PRs that show no sign of self-review will be closed without detailed feedback.

Keep it focused. One thing per PR. Don't bundle a bug fix with a refactor or unrelated cleanup.
If it's not ready, open it as a draft instead.


What does this PR do?

Displays every stored series membership on the book details tab instead of showing only the primary series fields.

  • Adds a sorted series link list from seriesMemberships
  • Links each membership to its series detail page when a series id is available
  • Keeps the existing primary series fields as a fallback when memberships are absent
  • Covers both detail header layouts with focused component tests

Closes #595

How did you test this?

  • pnpm exec vitest run src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.ts
    • Result: passed, 10 tests
  • pnpm exec eslint src/features/book/components/detail/tabs/DetailsTab.vue src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.ts --max-warnings 0
    • Result: passed

Screenshots

UI change is limited to rendering additional series links in the existing book details header. Screenshots can be added on GitHub with a book that has multiple series memberships.

Anything non-obvious in the diff?

The book details tab renders the series links in two responsive header layouts, so the test expects each series link to appear twice.

Checklist

  • I've read through my own diff
  • This PR was discussed in an issue and has maintainer approval (for new features)
  • Lint and tests pass locally
  • No unintended files included (build artifacts, .env, personal configs)

Summary by CodeRabbit

  • New Features

    • Book details now show multiple series memberships when available, instead of only a single series entry.
    • Series entries can link directly to their corresponding series pages when an ID is present.
    • If multiple series aren’t available, the app still displays the primary series information as before.
  • Bug Fixes

    • Improved series display consistency across mobile and desktop views.
    • Series labels now show index information in a clearer format.

- Render every stored series membership on book details.
- Link each displayed membership to its series detail route when an id is available.
- Keep the primary series fields as a fallback when memberships are absent.

Closes bookorbit#595
@github-actions github-actions Bot added the client Changes affecting the client application or frontend behavior. label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds multi-series support to the book details page. A SeriesDisplayLink type and seriesLinks computed replace the single seriesLine, deriving links from book.seriesMemberships with fallback to primary series fields. Mobile and desktop templates render these as links or text, with accompanying tests.

Changes

Multi-series links

Layer / File(s) Summary
Series link type and computed logic
client/src/features/book/components/detail/tabs/DetailsTab.vue
Adds SeriesDisplayLink type and formatSeriesLabel() helper, replacing seriesLine with seriesLinks computed built from book.seriesMemberships (sorted, labeled) or falling back to primary series fields.
Template rendering of series links
client/src/features/book/components/detail/tabs/DetailsTab.vue
Updates mobile and desktop identity sections to iterate seriesLinks, rendering RouterLink when seriesId exists, otherwise plain text.
Series link tests
client/src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.ts
Adds tests verifying links to series-detail for multiple memberships and fallback link generation when memberships are absent.

Estimated code review effort: 2 (Simple) | ~12 minutes

Poem

A rabbit hops through series and lore,
One book, many shelves, now more and more!
Links now bloom where once was one line,
Wax and Wayne, Mistborn, all intertwine 🐰📚
Hop click hop — to each series I go!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: showing multiple series memberships in the book details UI.
Description check ✅ Passed The description follows the template with purpose, issue reference, testing, screenshots, non-obvious notes, and checklist.
Linked Issues check ✅ Passed The PR implements the requested multiple-series display and navigation for book details, matching #595's core requirement.
Out of Scope Changes check ✅ Passed The diff stays focused on book details series rendering and tests, with no unrelated changes evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
client/src/features/book/components/detail/tabs/DetailsTab.vue (1)

806-810: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer Number.isInteger over modulo check.

seriesIndex % 1 === 0 works but Number.isInteger(seriesIndex) is the idiomatic way to test for whole numbers and is clearer to readers.

♻️ Optional refactor
 function formatSeriesLabel(seriesName: string, seriesIndex: number | null): string {
   if (seriesIndex == null) return seriesName
-  const formattedIndex = seriesIndex % 1 === 0 ? Math.floor(seriesIndex) : seriesIndex
+  const formattedIndex = Number.isInteger(seriesIndex) ? seriesIndex : seriesIndex
   return `${seriesName} #${formattedIndex}`
 }

Note: Number.isInteger already guarantees the value is whole, so Math.floor becomes unnecessary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/features/book/components/detail/tabs/DetailsTab.vue` around lines
806 - 810, The whole-number check in formatSeriesLabel should use the idiomatic
Number.isInteger test instead of the modulo expression. Update the logic in
formatSeriesLabel to check whether seriesIndex is an integer with
Number.isInteger(seriesIndex), and since that already guarantees a whole number,
remove the unnecessary Math.floor branch while keeping the same output
formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/features/book/components/detail/tabs/DetailsTab.vue`:
- Around line 812-824: The computed seriesLinks in DetailsTab.vue is returning
an empty array too early when seriesMemberships exists but all entries have
blank seriesName, which prevents the primary-series fallback from running.
Update the seriesLinks logic to only return mapped memberships when the filtered
list is non-empty, and otherwise allow the existing fallback branch that uses
props.book.seriesName and related fields to execute. Keep the fix centered on
the seriesLinks computed and its membership filtering/sorting path.

---

Nitpick comments:
In `@client/src/features/book/components/detail/tabs/DetailsTab.vue`:
- Around line 806-810: The whole-number check in formatSeriesLabel should use
the idiomatic Number.isInteger test instead of the modulo expression. Update the
logic in formatSeriesLabel to check whether seriesIndex is an integer with
Number.isInteger(seriesIndex), and since that already guarantees a whole number,
remove the unnecessary Math.floor branch while keeping the same output
formatting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4ad5c9f6-f070-4462-aa95-274c2bfb87d1

📥 Commits

Reviewing files that changed from the base of the PR and between daed6ce and e1c46b4.

📒 Files selected for processing (2)
  • client/src/features/book/components/detail/tabs/DetailsTab.vue
  • client/src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.ts

Comment on lines +812 to +824
const seriesLinks = computed<SeriesDisplayLink[]>(() => {
const memberships = props.book.seriesMemberships ?? []
if (memberships.length > 0) {
return memberships
.filter((membership) => membership.seriesName.trim().length > 0)
.slice()
.sort((a, b) => a.displayOrder - b.displayOrder || a.seriesId - b.seriesId)
.map((membership) => ({
key: `${membership.seriesId}-${membership.displayOrder}`,
seriesId: membership.seriesId,
label: formatSeriesLabel(membership.seriesName, membership.seriesIndex),
}))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fallback is skipped when all memberships have empty seriesName.

If seriesMemberships is non-empty but every entry has a blank seriesName, the .filter() produces an empty array and the computed returns []. The primary-series fallback at lines 826-833 is never reached because the outer if (memberships.length > 0) guard already passed. The user would see no series at all despite book.seriesName being populated.

🛡️ Proposed fix
 const seriesLinks = computed<SeriesDisplayLink[]>(() => {
   const memberships = props.book.seriesMemberships ?? []
   if (memberships.length > 0) {
-    return memberships
+    const links = memberships
       .filter((membership) => membership.seriesName.trim().length > 0)
       .slice()
       .sort((a, b) => a.displayOrder - b.displayOrder || a.seriesId - b.seriesId)
       .map((membership) => ({
         key: `${membership.seriesId}-${membership.displayOrder}`,
         seriesId: membership.seriesId,
         label: formatSeriesLabel(membership.seriesName, membership.seriesIndex),
       }))
+    if (links.length > 0) return links
   }
 
   if (!props.book.seriesName) return []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const seriesLinks = computed<SeriesDisplayLink[]>(() => {
const memberships = props.book.seriesMemberships ?? []
if (memberships.length > 0) {
return memberships
.filter((membership) => membership.seriesName.trim().length > 0)
.slice()
.sort((a, b) => a.displayOrder - b.displayOrder || a.seriesId - b.seriesId)
.map((membership) => ({
key: `${membership.seriesId}-${membership.displayOrder}`,
seriesId: membership.seriesId,
label: formatSeriesLabel(membership.seriesName, membership.seriesIndex),
}))
}
const seriesLinks = computed<SeriesDisplayLink[]>(() => {
const memberships = props.book.seriesMemberships ?? []
if (memberships.length > 0) {
const links = memberships
.filter((membership) => membership.seriesName.trim().length > 0)
.slice()
.sort((a, b) => a.displayOrder - b.displayOrder || a.seriesId - b.seriesId)
.map((membership) => ({
key: `${membership.seriesId}-${membership.displayOrder}`,
seriesId: membership.seriesId,
label: formatSeriesLabel(membership.seriesName, membership.seriesIndex),
}))
if (links.length > 0) return links
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/features/book/components/detail/tabs/DetailsTab.vue` around lines
812 - 824, The computed seriesLinks in DetailsTab.vue is returning an empty
array too early when seriesMemberships exists but all entries have blank
seriesName, which prevents the primary-series fallback from running. Update the
seriesLinks logic to only return mapped memberships when the filtered list is
non-empty, and otherwise allow the existing fallback branch that uses
props.book.seriesName and related fields to execute. Keep the fix centered on
the seriesLinks computed and its membership filtering/sorting path.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.19048% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...eatures/book/components/detail/tabs/DetailsTab.vue 76.19% 0 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

@neonsolstice
neonsolstice merged commit acf6371 into bookorbit:main Jul 9, 2026
25 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.2.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions github-actions Bot added the released Issue or PR is included in a released version. label Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client Changes affecting the client application or frontend behavior. released Issue or PR is included in a released version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] For books that belong in multiple series, display all relevant series in the book details page

2 participants