1
0
mirror of synced 2025-12-19 09:57:42 -05:00
Files
docs/src/frame/components/context/MainContext.tsx
2025-10-21 15:00:13 +00:00

295 lines
9.1 KiB
TypeScript

import { createContext, useContext } from 'react'
import pick from 'lodash/pick'
import type { BreadcrumbT } from '@/frame/components/page-header/Breadcrumbs'
import type { FeatureFlags } from '@/frame/components/hooks/useFeatureFlags'
import type { SidebarLink } from '@/types'
export type ProductT = {
external: boolean
href: string
id: string
name: string
nameRendered: string
}
export type VersionItem = {
// free-pro-team@latest, enterprise-cloud@latest, enterprise-server@3.3 ...
version: string
versionTitle: string
isGHES?: boolean
apiVersions: string[]
latestApiVersion: string
}
// This reflects what gets exported from `all-versions.ts` in the
// `allVersions` object.
// It's necessary for TypeScript, but we don't need to write down
// every possible key that might be present because we don't need it
// for rendering.
type FullVersionItem = VersionItem & {
shortName: string
}
function minimalAllVersions(
allVersions: Record<string, FullVersionItem>,
): Record<string, VersionItem> {
const all: Record<string, VersionItem> = {}
for (const [plan, info] of Object.entries(allVersions)) {
all[plan] = {
version: info.version,
versionTitle: info.versionTitle,
apiVersions: info.apiVersions,
latestApiVersion: info.latestApiVersion,
}
// Deal with keys that are optional. It's preferred to omit
// booleans if they're false anyway.
if (info.shortName === 'ghes') {
all[plan].isGHES = true
}
}
return all
}
export type ProductTreeNode = {
title: string
href: string
childPages: Array<ProductTreeNode>
sidebarLink?: SidebarLink
layout?: string
}
type UIString = Record<string, string>
export type UIStrings = UIString | { [key: string]: UIStrings }
export type EnterpriseDeprecation = {
version_was_deprecated: string
version_will_be_deprecated: string
deprecation_details: string
isOldestReleaseDeprecated?: boolean
}
type DataReusables = {
enterprise_deprecation?: EnterpriseDeprecation
}
type DataT = {
ui: UIStrings
reusables: DataReusables
variables: {
release_candidate: { version: string | null }
}
}
type EnterpriseServerReleases = {
isOldestReleaseDeprecated: boolean
oldestSupported: string
nextDeprecationDate: string
supported: Array<string>
}
export type MainContextT = {
allVersions: Record<string, VersionItem>
breadcrumbs: {
product: BreadcrumbT
category?: BreadcrumbT
subcategory?: BreadcrumbT
article?: BreadcrumbT
}
communityRedirect: {
name: string
href: string
}
currentCategory?: string
currentPathWithoutLanguage: string
currentProduct?: ProductT
currentProductName: string
currentProductTree?: ProductTreeNode | null
currentLayoutName?: string
currentVersion?: string
data: DataT
enterpriseServerReleases: EnterpriseServerReleases
enterpriseServerVersions: Array<string>
error: string
featureFlags: FeatureFlags
fullUrl: string
isHomepageVersion: boolean
nonEnterpriseDefaultVersion: string
page: {
documentType: string
type?: string
topics: Array<string>
title: string
fullTitle?: string
introPlainText?: string
hidden: boolean
noEarlyAccessBanner: boolean
applicableVersions: string[]
} | null
relativePath?: string
sidebarTree?: ProductTreeNode | null
status: number
xHost?: string
}
// Write down the namespaces from `data/ui.yml` that are used on all pages,
// they will always be available and don't need to be manually added.
// Order does not matter on these.
const DEFAULT_UI_NAMESPACES = [
'alerts',
'header',
'search',
'old_search',
'survey',
'toc',
'meta',
'scroll_button',
'pages',
'picker',
'footer',
'contribution_cta',
'support',
'rest',
'cookbook_landing',
]
export function addUINamespaces(req: any, ui: UIStrings, namespaces: string[]) {
const pool = req.context.site.data.ui
for (const namespace of namespaces) {
if (!(namespace in pool)) {
throw new Error(
`Invalid namespace "${namespace}". It's not present in data/ui.yml as a namespace. (not one of: ${Object.keys(
pool,
)})`,
)
}
ui[namespace] = pool[namespace]
}
}
export const getMainContext = async (req: any, res: any): Promise<MainContextT> => {
// Our current translation process adds 'ms.*' frontmatter properties to files
// it translates including when data/ui.yml is translated. We don't use these
// properties and their syntax (e.g. 'ms.openlocfilehash',
// 'ms.sourcegitcommit', etc.) causes problems so just delete them.
if (req.context.site.data.ui.ms) {
delete req.context.site.data.ui.ms
}
const { page } = req.context
const documentType = page ? (page.documentType as string) : undefined
const ui: UIStrings = {}
addUINamespaces(req, ui, DEFAULT_UI_NAMESPACES)
// Every product landing page has a listing of all articles.
// It's used by the <ProductArticlesList> component.
const includeFullProductTree = documentType === 'product'
const includeSidebarTree = documentType !== 'homepage'
const reusables: DataReusables = {}
// To know whether we need this key, we need to match this
// with the business logic in `DeprecationBanner.tsx` which is as follows:
if (req.context.currentVersion.includes(req.context.enterpriseServerReleases.oldestSupported)) {
reusables.enterprise_deprecation = {
version_was_deprecated: req.context.getDottedData(
'reusables.enterprise_deprecation.version_was_deprecated',
),
version_will_be_deprecated: req.context.getDottedData(
'reusables.enterprise_deprecation.version_will_be_deprecated',
),
deprecation_details: req.context.getDottedData(
'reusables.enterprise_deprecation.deprecation_details',
),
}
}
// This is a number, like 3.13 or it's possibly null if there is no
// supported release candidate at the moment.
const { releaseCandidate } = req.context.enterpriseServerReleases
// Combine the version number with the prefix so it can appear
// as a full version string if the release candidate is set.
const releaseCandidateVersion = releaseCandidate ? `enterprise-server@${releaseCandidate}` : null
const pageInfo =
(page && {
documentType,
type: req.context.page.type || null,
title: req.context.page.title,
fullTitle: req.context.page.fullTitle || null,
topics: req.context.page.topics || [],
introPlainText: req.context.page?.introPlainText || null,
applicableVersions: req.context.page?.permalinks.map((obj: any) => obj.pageVersion) || [],
hidden: req.context.page.hidden || false,
noEarlyAccessBanner: req.context.page.noEarlyAccessBanner || false,
}) ||
null
const currentProduct: ProductT = req.context.productMap[req.context.currentProduct] || null
const currentProductName: string = req.context.currentProductName || ''
const props: MainContextT = {
allVersions: minimalAllVersions(req.context.allVersions),
breadcrumbs: req.context.breadcrumbs || {},
communityRedirect: req.context.page?.communityRedirect || {},
currentCategory: req.context.currentCategory || '',
currentLayoutName: req.context.currentLayoutName || null,
currentPathWithoutLanguage: req.context.currentPathWithoutLanguage,
currentProduct,
currentProductName,
// This is a slimmed down version of `req.context.currentProductTree`
// that only has the minimal titles stuff needed for sidebars and
// any page that is hidden is omitted.
// However, it's not needed on most pages. For example, on article pages,
// you don't need it. It's similar to the minimal product tree but,
// has the full length titles and not just the short titles.
currentProductTree:
(includeFullProductTree && req.context.currentProductTreeTitlesExcludeHidden) || null,
currentVersion: req.context.currentVersion,
data: {
ui,
reusables,
variables: {
release_candidate: {
version: releaseCandidateVersion,
},
},
},
enterpriseServerReleases: pick(req.context.enterpriseServerReleases, [
'isOldestReleaseDeprecated',
'oldestSupported',
'nextDeprecationDate',
'supported',
]),
enterpriseServerVersions: req.context.enterpriseServerVersions,
error: req.context.error ? req.context.error.toString() : '',
featureFlags: {},
fullUrl: req.protocol + '://' + req.hostname + req.originalUrl, // does not include port for localhost
isHomepageVersion: req.context.page?.documentType === 'homepage',
nonEnterpriseDefaultVersion: req.context.nonEnterpriseDefaultVersion,
page: pageInfo,
relativePath: req.context.page?.relativePath || null,
// The minimal product tree is needed on all pages that depend on
// the product sidebar or the rest sidebar.
sidebarTree: (includeSidebarTree && req.context.sidebarTree) || null,
status: res.statusCode,
xHost: req.get('x-host') || '',
}
return props
}
export const MainContext = createContext<MainContextT | null>(null)
export const useMainContext = (): MainContextT => {
const context = useContext(MainContext)
if (!context) {
throw new Error('"useMainContext" may only be used inside "MainContext.Provider"')
}
return context
}