diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index dcd8e8fb7f..89d6cce7a0 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -25,12 +25,12 @@ export function Header(props: { }; }) { const { context, withTopHeader, variants } = props; - const { siteSpace, siteSpaces, sections, customization } = context; + const { siteSpace, visibleSiteSpaces, visibleSections, customization } = context; const withSections = Boolean( - sections && - (sections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group - sections.list.some((s) => s.object === 'site-section-group')) + visibleSections && + (visibleSections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group + visibleSections.list.some((s) => s.object === 'site-section-group')) ); return ( @@ -139,20 +139,22 @@ export function Header(props: { style={customization.styling.search} withVariants={variants.generic.length > 1} withSiteVariants={ - sections?.list.some( + visibleSections?.list.some( (s) => s.object === 'site-section' && s.siteSpaces.length > 1 ) ?? false } - withSections={sections ? sections.list.length > 1 : false} + withSections={ + visibleSections ? visibleSections.list.length > 1 : false + } section={ - sections - ? // Client-encode to avoid a serialisation issue that was causing the language selector to disappear - encodeClientSiteSections(context, sections).current + visibleSections + ? // Client-encode to avoid a serialization issue that was causing the language selector to disappear + encodeClientSiteSections(context, visibleSections).current : undefined } siteSpace={siteSpace} - siteSpaces={siteSpaces} + siteSpaces={visibleSiteSpaces} viewport={!withTopHeader ? 'mobile' : undefined} /> @@ -196,9 +198,9 @@ export function Header(props: { - {sections && withSections ? ( + {visibleSections && withSections ? (
- + {variants.translations.length > 1 ? ( 0); + const withSections = Boolean(visibleSections && visibleSections.list.length > 0); const document = await getPageDocument(context, page); diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts index 1b210ac669..a43c06d6aa 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts @@ -14,6 +14,7 @@ function makeContext(current: FakeSiteSpace, all: FakeSiteSpace[]) { // Only the properties used by categorizeVariants are required for these tests siteSpace: current, siteSpaces: all, + visibleSiteSpaces: all, } as unknown as Parameters[0]; } diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 2f2f2870b8..36097a3bc8 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -99,11 +99,11 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) { */ export function SpaceLayout(props: SpaceLayoutProps) { const { context, children } = props; - const { siteSpace, customization, sections, siteSpaces } = context; + const { siteSpace, customization, visibleSections, visibleSiteSpaces } = context; const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None; - const withSections = Boolean(sections && sections.list.length > 1); + const withSections = Boolean(visibleSections && visibleSections.list.length > 1); const variants = categorizeVariants(context); const withFooter = @@ -181,25 +181,28 @@ export function SpaceLayout(props: SpaceLayoutProps) { style={CustomizationSearchStyle.Subtle} withVariants={variants.generic.length > 1} withSiteVariants={ - sections?.list.some( + visibleSections?.list.some( (s) => s.object === 'site-section' && s.siteSpaces.length > 1 ) ?? false } withSections={withSections} - section={sections?.current} + section={visibleSections?.current} siteSpace={siteSpace} - siteSpaces={siteSpaces} + siteSpaces={visibleSiteSpaces} className="max-lg:hidden" viewport="desktop" />
)} - {!withTopHeader && withSections && sections && ( + {!withTopHeader && withSections && visibleSections && ( )} {variants.generic.length > 1 ? ( diff --git a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts index 94df27104f..81064f59fd 100644 --- a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts +++ b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts @@ -5,7 +5,7 @@ import type { GitBookSiteContext } from '@/lib/context'; * Categorize the variants of the space into generic and translation variants. */ export function categorizeVariants(context: GitBookSiteContext) { - const { siteSpace, siteSpaces } = context; + const { siteSpace, visibleSiteSpaces: siteSpaces } = context; const currentLanguage = siteSpace.space.language; // Get all languages of the variants. diff --git a/packages/gitbook/src/lib/context.ts b/packages/gitbook/src/lib/context.ts index 320c8e7c8d..f619b39a6b 100644 --- a/packages/gitbook/src/lib/context.ts +++ b/packages/gitbook/src/lib/context.ts @@ -120,9 +120,15 @@ export type GitBookSiteContext = GitBookSpaceContext & { /** All site spaces in the current section / or entire site */ siteSpaces: SiteSpace[]; + /** Site spaces that are not hidden (visible to visitors). */ + visibleSiteSpaces: SiteSpace[]; + /** Sections of the site. */ sections: null | SiteSections; + /** Sections filtered to visible site spaces only. */ + visibleSections: null | SiteSections; + /** Customizations of the site. */ customization: SiteCustomizationSettings; @@ -261,9 +267,16 @@ export async function fetchSiteContextByIds( const sections = ids.siteSection ? parseSiteSectionsAndGroups(siteStructure, ids.siteSection) : null; + const visibleSections = ids.siteSection + ? parseVisibleSiteSectionsAndGroups(siteStructure, ids.siteSection) + : null; // Parse the current siteSpace and siteSpaces based on the site structure type. - const { siteSpaces, siteSpace }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace } = (() => { + const { + siteSpaces, + siteSpace, + visibleSiteSpaces, + }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace; visibleSiteSpaces: SiteSpace[] } = (() => { if (siteStructure.type === 'siteSpaces') { const siteSpaces = siteStructure.structure; const siteSpace = siteSpaces.find((siteSpace) => siteSpace.id === ids.siteSpace); @@ -274,7 +287,7 @@ export async function fetchSiteContextByIds( ); } - return { siteSpaces: filterHiddenSiteSpaces(siteSpaces), siteSpace }; + return { siteSpaces, siteSpace, visibleSiteSpaces: filterHiddenSiteSpaces(siteSpaces) }; } if (siteStructure.type === 'sections') { @@ -295,7 +308,11 @@ export async function fetchSiteContextByIds( ); } - return { siteSpaces: filterHiddenSiteSpaces(siteSpaces), siteSpace }; + return { + siteSpaces, + siteSpace, + visibleSiteSpaces: filterHiddenSiteSpaces(siteSpaces), + }; } // @ts-expect-error @@ -327,10 +344,12 @@ export async function fetchSiteContextByIds( organizationId: ids.organization, site, siteSpaces, + visibleSiteSpaces, siteSpace, customization, structure: siteStructure, sections, + visibleSections, scripts, contextId: ids.contextId, isFallback: ids.isFallback, @@ -434,13 +453,58 @@ function filterHiddenSiteSpaces(siteSpaces: SiteSpace[]): SiteSpace[] { } function parseSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) { - const sectionsAndGroups = getSiteStructureSections(structure, { ignoreGroups: false }); + const sectionsAndGroups = getSiteStructureSections(structure); const section = parseCurrentSection(structure, siteSectionId); assert(section, `couldn't find section "${siteSectionId}" in site structure`); return { list: sectionsAndGroups, current: section } satisfies SiteSections; } +function parseVisibleSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) { + const { list: sectionsAndGroups, current: section } = parseSiteSectionsAndGroups( + structure, + siteSectionId + ); + const visibleSectionsAndGroups = filterSectionsAndGroupsWithHiddenSiteSpaces(sectionsAndGroups); + const current = section && !sectionHasOnlyHiddenSiteSpaces(section) ? section : null; + assert(current, `couldn't find section "${siteSectionId}" in site structure`); + return { list: visibleSectionsAndGroups, current } satisfies SiteSections; +} + function parseCurrentSection(structure: SiteStructure, siteSectionId: string) { const sections = getSiteStructureSections(structure, { ignoreGroups: true }); return sections.find((section) => section.id === siteSectionId); } + +type SectionOrGroup = SiteSection | SiteSectionGroup; + +/** + * Filter out sections where all site spaces are hidden and groups that become empty after filtering. + */ +function filterSectionsAndGroupsWithHiddenSiteSpaces( + sectionsOrGroups: SectionOrGroup[] +): SectionOrGroup[] { + return sectionsOrGroups + .map((entry) => { + if (entry.object === 'site-section') { + return sectionHasOnlyHiddenSiteSpaces(entry) ? null : entry; + } + + const visibleChildren: SectionOrGroup[] = filterSectionsAndGroupsWithHiddenSiteSpaces( + entry.children + ); + + if (visibleChildren.length === 0) { + return null; + } + + return { + ...entry, + children: visibleChildren, + }; + }) + .filter((entry): entry is SiteSection | SiteSectionGroup => Boolean(entry)); +} + +function sectionHasOnlyHiddenSiteSpaces(section: SiteSection) { + return section.siteSpaces.every((siteSpace) => siteSpace.hidden); +}