Skip to content

Commit f0e588c

Browse files
os-zhuangclaude
andcommitted
fix(console): authored books lead the index; humanize implicit book labels
Browser-verified against the framework showcase backend. Two polish fixes found there: - buildPortalBooks now returns authored books (in their `order`) ahead of the synthetic per-package books, and buildBookCards preserves that order instead of re-sorting — so a book with an explicit `order: 0` (e.g. the showcase manual) is no longer pushed below a fallback implicit book. - Implicit per-package books label themselves with the humanized package id ("com.objectstack.setup" → "Setup") rather than the raw id; the slug/name stay the full id so they remain unique and reversible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dc9dd98 commit f0e588c

3 files changed

Lines changed: 29 additions & 7 deletions

File tree

apps/console/src/pages/book-nav.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ describe('buildPortalBooks (ADR-0046 §6.4 — no flat/book fork)', () => {
138138
expect(portal.map((b) => b.name)).toContain('misc'); // implicit book keyed by packageId
139139
// no duplicate implicit 'crm' book, since crm_manual already owns 'crm'
140140
expect(portal.filter((b) => b.name === 'crm').length).toBe(0);
141+
// authored books lead, implicit ones follow
142+
expect(portal[0].name).toBe('crm_manual');
143+
expect(portal[portal.length - 1].name).toBe('misc');
144+
// implicit label is humanized, not the raw package id
145+
expect(portal.find((b) => b.name === 'misc')?.label).toBe('Misc');
141146
});
142147

143148
it('with no authored books, every package becomes its own implicit book', () => {

apps/console/src/pages/book-nav.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,14 @@ export function countBookDocs(book: Book, docs: ResolverDoc[]): number {
323323
return seen.size;
324324
}
325325

326-
/** Build the index cards for the portal landing, in display order. */
326+
/**
327+
* Build the index cards for the portal landing. Input order is preserved —
328+
* callers pass {@link buildPortalBooks} output, which already orders authored
329+
* books (by `order`) ahead of the synthetic per-package ones; re-sorting here
330+
* would undo that.
331+
*/
327332
export function buildBookCards(books: Book[], docs: ResolverDoc[]): BookCard[] {
328-
return sortBooks(books).map((b) => ({
333+
return books.map((b) => ({
329334
name: b.name,
330335
slug: bookSlug(b),
331336
label: b.label ?? b.name,
@@ -348,8 +353,20 @@ export function buildPortalBooks(authored: Book[], docs: ResolverDoc[]): Book[]
348353
const docPkgs = new Set(docs.map(pkgOf));
349354
const implicit: Book[] = [...docPkgs]
350355
.filter((p) => !ownPkgs.has(p))
351-
.map((p) => ({ ...deriveImplicitPackageBook(p), packageId: p }));
352-
return sortBooks([...authored, ...implicit]);
356+
// Humanize the package id for the visible label (the slug/name stays the
357+
// full id so it remains unique and reversible); keep the group as the
358+
// generic "Documentation".
359+
.map((p) => ({ ...deriveImplicitPackageBook(p), label: humanizePackageId(p), packageId: p }));
360+
// Authored books lead (curated, primary) in their own order; the synthetic
361+
// per-package books follow, alphabetically — so an authored book with an
362+
// explicit `order` is never pushed below a fallback.
363+
return [...sortBooks(authored), ...sortBooks(implicit)];
364+
}
365+
366+
/** "com.objectstack.setup" → "Setup" — a readable label for an implicit book. */
367+
function humanizePackageId(pkg: string): string {
368+
const seg = pkg.split('.').pop() || pkg;
369+
return seg.charAt(0).toUpperCase() + seg.slice(1);
353370
}
354371

355372
/**

apps/console/src/pages/docs-portal.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ describe('book-driven docs portal (integration)', () => {
126126
it('clicking a book card navigates to its landing (real router flow)', async () => {
127127
render(<Harness entry="/docs" />);
128128
fireEvent.click(await screen.findByRole('link', { name: /ops/i }));
129-
// Now on /docs/ops — the ops book landing renders, headed by the book, with
130-
// its doc reachable (in both the sidebar and the overview list).
131-
expect(await screen.findByRole('heading', { name: 'ops' })).toBeInTheDocument();
129+
// Now on /docs/ops — the ops book landing renders, headed by the book
130+
// (its label is the humanized package id, "Ops"), with its doc reachable.
131+
expect(await screen.findByRole('heading', { name: /ops/i })).toBeInTheDocument();
132132
const setupLinks = screen.getAllByRole('link', { name: 'Setup' });
133133
expect(setupLinks.some((a) => a.getAttribute('href') === '/docs/ops/ops_setup')).toBe(true);
134134
});

0 commit comments

Comments
 (0)