Update docs to use Primer React 37 (#57265)
Co-authored-by: Evan Bonsignori <evanabonsignori@gmail.com>
This commit is contained in:
@@ -15,6 +15,9 @@ const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
|
||||
const productIds = data.children
|
||||
|
||||
export default {
|
||||
// Transpile @primer/react so Next's webpack can process its CSS and other assets
|
||||
// This ensures CSS in node_modules/@primer/react is handled by the app's loaders.
|
||||
transpilePackages: ['@primer/react'],
|
||||
// speed up production `next build` by ignoring typechecking during that step of build.
|
||||
// type-checking still occurs in the Dockerfile build
|
||||
typescript: {
|
||||
|
||||
1019
package-lock.json
generated
1019
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -166,7 +166,7 @@
|
||||
"@primer/live-region-element": "^0.7.2",
|
||||
"@primer/octicons": "^19.15.5",
|
||||
"@primer/octicons-react": "^19.14.0",
|
||||
"@primer/react": "36.27.0",
|
||||
"@primer/react": "^37.31.0",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-errors": "^3.0.0",
|
||||
@@ -177,6 +177,7 @@
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cheerio-to-text": "0.2.4",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"connect-timeout": "1.9.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cuss": "2.2.0",
|
||||
@@ -219,8 +220,8 @@
|
||||
"ora": "^8.0.1",
|
||||
"parse5": "7.1.2",
|
||||
"quick-lru": "7.0.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
@@ -526,7 +526,7 @@ test.describe('test nav at different viewports', () => {
|
||||
// hamburger button for sidebar overlay is visible
|
||||
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
|
||||
await page.getByTestId('sidebar-hamburger').click()
|
||||
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible()
|
||||
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('medium viewports - 768-1011', async ({ page }) => {
|
||||
@@ -555,7 +555,7 @@ test.describe('test nav at different viewports', () => {
|
||||
// hamburger button for sidebar overlay is visible
|
||||
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
|
||||
await page.getByTestId('sidebar-hamburger').click()
|
||||
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible()
|
||||
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('small viewports - 544-767', async ({ page }) => {
|
||||
@@ -588,7 +588,7 @@ test.describe('test nav at different viewports', () => {
|
||||
// hamburger button for sidebar overlay is visible
|
||||
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
|
||||
await page.getByTestId('sidebar-hamburger').click()
|
||||
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible()
|
||||
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('x-small viewports - 0-544', async ({ page }) => {
|
||||
@@ -627,7 +627,7 @@ test.describe('test nav at different viewports', () => {
|
||||
// hamburger button for sidebar overlay is visible
|
||||
await expect(page.getByTestId('sidebar-hamburger')).toBeVisible()
|
||||
await page.getByTestId('sidebar-hamburger').click()
|
||||
await expect(page.getByTestId('sidebar-product-dialog')).toBeVisible()
|
||||
await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('do a search when the viewport is x-small', async ({ page }) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('sidebar', () => {
|
||||
const $: cheerio.Root = await getDOM('/get-started/start-your-journey/hello-world')
|
||||
expect(
|
||||
$(
|
||||
'[data-testid=sidebar] [data-testid=product-sidebar] a[aria-current="page"] div span',
|
||||
'[data-testid=sidebar] [data-testid=product-sidebar] a[aria-current="page"] span span',
|
||||
).text(),
|
||||
).toBe('Hello World')
|
||||
})
|
||||
@@ -35,7 +35,7 @@ describe('sidebar', () => {
|
||||
// from its regular title.
|
||||
expect(
|
||||
$(
|
||||
'[data-testid=sidebar] [data-testid=product-sidebar] a[href*="/get-started/foo/bar"] div span',
|
||||
'[data-testid=sidebar] [data-testid=product-sidebar] a[href*="/get-started/foo/bar"] span span',
|
||||
).text(),
|
||||
).toBe('Bar')
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "@primer/css/support/variables/layout.scss";
|
||||
@import "@primer/css/support/mixins/layout.scss";
|
||||
@import "src/frame/stylesheets/breakpoint-xxl.scss";
|
||||
|
||||
.header {
|
||||
display: unset;
|
||||
@@ -51,3 +52,13 @@
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@include breakpoint-xxl {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[class*="prc-Dialog-Body"] {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useTranslation } from '@/languages/components/useTranslation'
|
||||
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
|
||||
import { VersionPicker } from '@/versions/components/VersionPicker'
|
||||
import { SidebarNav } from '@/frame/components/sidebar/SidebarNav'
|
||||
import { AllProductsLink } from '@/frame/components/sidebar/AllProductsLink'
|
||||
import { SearchBarButton } from '@/search/components/input/SearchBarButton'
|
||||
import { HeaderSearchAndWidgets } from './HeaderSearchAndWidgets'
|
||||
import { useInnerWindowWidth } from './hooks/useInnerWindowWidth'
|
||||
@@ -34,8 +33,8 @@ export const Header = () => {
|
||||
const { params, updateParams } = useMultiQueryParams()
|
||||
const [scroll, setScroll] = useState(false)
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
||||
const openSidebar = useCallback(() => setIsSidebarOpen(true), [isSidebarOpen])
|
||||
const closeSidebar = useCallback(() => setIsSidebarOpen(false), [isSidebarOpen])
|
||||
const openSidebar = useCallback(() => setIsSidebarOpen(true), [])
|
||||
const closeSidebar = useCallback(() => setIsSidebarOpen(false), [])
|
||||
const isMounted = useRef(false)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const { asPath } = useRouter()
|
||||
@@ -204,45 +203,23 @@ export const Header = () => {
|
||||
onClick={openSidebar}
|
||||
ref={returnFocusRef}
|
||||
/>
|
||||
<Dialog
|
||||
returnFocusRef={returnFocusRef}
|
||||
isOpen={isSidebarOpen}
|
||||
onDismiss={closeSidebar}
|
||||
aria-labelledby="menu-title"
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
marginTop: '0',
|
||||
maxHeight: '100vh',
|
||||
width: 'auto !important',
|
||||
transform: 'none',
|
||||
borderRadius: '0',
|
||||
borderRight:
|
||||
'1px solid var(--borderColor-default, var(--color-border-default))',
|
||||
}}
|
||||
>
|
||||
<Dialog.Header
|
||||
style={{ paddingTop: '0px', background: 'none' }}
|
||||
id="sidebar-overlay-header"
|
||||
sx={{ display: 'block' }}
|
||||
{isSidebarOpen && (
|
||||
<Dialog
|
||||
returnFocusRef={returnFocusRef}
|
||||
onClose={closeSidebar}
|
||||
className={cx(styles.dialog, 'd-xxl-none')}
|
||||
position="left"
|
||||
title={
|
||||
error === '404' || !currentProduct || isSearchResultsPage
|
||||
? null
|
||||
: currentProductName || currentProduct.name
|
||||
}
|
||||
subtitle={isRestPage && <ApiVersionPicker />}
|
||||
width="medium"
|
||||
>
|
||||
<AllProductsLink />
|
||||
{error === '404' || !currentProduct || isSearchResultsPage ? null : (
|
||||
<h2 className="mt-3">
|
||||
<Link
|
||||
data-testid="sidebar-product-dialog"
|
||||
href={currentProduct.href}
|
||||
className="d-block pl-1 mb-2 h3 color-fg-default no-underline"
|
||||
>
|
||||
{currentProductName || currentProduct.name}
|
||||
</Link>
|
||||
</h2>
|
||||
)}
|
||||
{isRestPage && <ApiVersionPicker />}
|
||||
</Dialog.Header>
|
||||
<SidebarNav variant="overlay" />
|
||||
</Dialog>
|
||||
<SidebarNav variant="overlay" />
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mr-auto width-full" data-search="breadcrumbs">
|
||||
|
||||
@@ -33,7 +33,9 @@ export const SidebarNav = ({ variant = 'full' }: Props) => {
|
||||
<div
|
||||
data-container="nav"
|
||||
className={cx(variant === 'full' ? 'position-sticky d-none border-right d-xxl-block' : '')}
|
||||
style={{ width: 326, height: 'calc(100vh - 65px)', top: '65px' }}
|
||||
style={
|
||||
variant === 'full' ? { width: 326, height: 'calc(100vh - 65px)', top: '65px' } : undefined
|
||||
}
|
||||
>
|
||||
<nav
|
||||
aria-labelledby="allproducts-menu"
|
||||
@@ -62,10 +64,16 @@ export const SidebarNav = ({ variant = 'full' }: Props) => {
|
||||
)}
|
||||
<div
|
||||
className={cx(
|
||||
variant === 'overlay' ? 'd-xxl-none' : 'border-right d-none d-xxl-block',
|
||||
'bg-primary overflow-y-auto flex-shrink-0',
|
||||
variant === 'overlay'
|
||||
? 'width-full d-xxl-none'
|
||||
: 'border-right d-none d-xxl-block overflow-y-auto',
|
||||
'bg-primary flex-shrink-0',
|
||||
)}
|
||||
style={{ width: 326, height: 'calc(100vh - 175px)', paddingBottom: sidebarPaddingBottom }}
|
||||
style={
|
||||
variant === 'overlay'
|
||||
? { paddingBottom: sidebarPaddingBottom }
|
||||
: { width: 326, height: 'calc(100vh - 175px)', paddingBottom: sidebarPaddingBottom }
|
||||
}
|
||||
role="region"
|
||||
aria-label="Page navigation content"
|
||||
>
|
||||
|
||||
12
src/frame/stylesheets/dialog-overrides.scss
Normal file
12
src/frame/stylesheets/dialog-overrides.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import "breakpoint-xxl.scss";
|
||||
|
||||
#__primerPortalRoot__ [class*="prc-Dialog-Backdrop"] {
|
||||
/* Make sure the dialog backdrop is hidden on large screens */
|
||||
display: flex;
|
||||
visibility: visible;
|
||||
|
||||
@include breakpoint-xxl {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
@import "headings.scss";
|
||||
@import "scroll-top.scss";
|
||||
@import "utilities.scss";
|
||||
@import "dialog-overrides.scss";
|
||||
|
||||
@import "src/content-render/stylesheets/index.scss";
|
||||
@import "src/links/stylesheets/hover-card.scss";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import { readFile } from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
|
||||
import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file'
|
||||
import { getOpenApiVersion } from '@/versions/lib/all-versions'
|
||||
@@ -10,7 +10,7 @@ const githubAppsData = new Map()
|
||||
|
||||
// Initialize the Map with the page type keys listed under `pages`
|
||||
// in the config.json file.
|
||||
const appsDataConfig = JSON.parse(await readFile('src/github-apps/lib/config.json', 'utf8'))
|
||||
const appsDataConfig = JSON.parse(fs.readFileSync('src/github-apps/lib/config.json', 'utf8'))
|
||||
for (const pageType of Object.keys(appsDataConfig.pages)) {
|
||||
githubAppsData.set(pageType, new Map())
|
||||
}
|
||||
|
||||
11
src/landings/components/ArticleList.module.css
Normal file
11
src/landings/components/ArticleList.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.linkItem {
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--fgColor-accent, var(--color-accent-fg));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import cx from 'classnames'
|
||||
import dayjs from 'dayjs'
|
||||
import { ActionList } from '@primer/react'
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
import { Link } from '@/frame/components/Link'
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
import { FeaturedLink } from '@/landings/components/ProductLandingContext'
|
||||
import { BumpLink } from '@/frame/components/ui/BumpLink'
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
import { ArrowRightIcon } from '@primer/octicons-react'
|
||||
import { ActionList } from '@primer/react'
|
||||
import { clsx } from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import styles from './ArticleList.module.css'
|
||||
|
||||
export type ArticleListPropsT = {
|
||||
title?: string
|
||||
@@ -29,7 +29,7 @@ export const ArticleList = ({
|
||||
<>
|
||||
{title && (
|
||||
<div className="mb-4 d-flex flex-items-baseline">
|
||||
<h2 className={cx('f4 text-semibold')}>{title}</h2>
|
||||
<h2 className={clsx('f4', 'text-semibold')}>{title}</h2>
|
||||
{viewAllHref && (
|
||||
<Link
|
||||
href={viewAllHref}
|
||||
@@ -44,37 +44,26 @@ export const ArticleList = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ActionList as="ul" data-testid="article-list" variant="full">
|
||||
<ActionList data-testid="article-list" variant="full">
|
||||
{articles.map((link) => {
|
||||
return (
|
||||
<ActionList.Item
|
||||
as="li"
|
||||
<ActionList.LinkItem
|
||||
as="a"
|
||||
key={link.href}
|
||||
className={cx('width-full border-top')}
|
||||
sx={{
|
||||
borderRadius: 0,
|
||||
':hover': {
|
||||
borderRadius: 0,
|
||||
},
|
||||
}}
|
||||
tabIndex={undefined}
|
||||
href={link.href}
|
||||
data-testid="bump-link"
|
||||
className={clsx(styles.linkItem, 'width-full', 'border-top', 'py-3')}
|
||||
>
|
||||
<BumpLink
|
||||
as={Link}
|
||||
href={link.href}
|
||||
className="py-3"
|
||||
title={
|
||||
link.intro ? (
|
||||
<h3 className="f4" data-testid="link-with-intro-title">
|
||||
<span>{link.fullTitle ? link.fullTitle : link.title}</span>
|
||||
</h3>
|
||||
) : (
|
||||
<span className="f4 text-bold d-block" data-testid="link-with-intro-title">
|
||||
{link.fullTitle ? link.fullTitle : link.title}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{link.intro ? (
|
||||
<h3 className="f4" data-testid="link-with-intro-title">
|
||||
<span>{link.fullTitle ? link.fullTitle : link.title}</span>
|
||||
</h3>
|
||||
) : (
|
||||
<span className="f4 text-bold d-block" data-testid="link-with-intro-title">
|
||||
{link.fullTitle ? link.fullTitle : link.title}
|
||||
</span>
|
||||
)}
|
||||
{link.intro && (
|
||||
<p className="color-fg-muted mb-0 mt-1" data-testid="link-with-intro-intro">
|
||||
{link.intro}
|
||||
@@ -88,8 +77,8 @@ export const ArticleList = ({
|
||||
{dayjs(link.date).format('MMMM DD')}
|
||||
</time>
|
||||
)}
|
||||
</BumpLink>
|
||||
</ActionList.Item>
|
||||
</div>
|
||||
</ActionList.LinkItem>
|
||||
)
|
||||
})}
|
||||
</ActionList>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const CookBookArticleCard = ({
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="h4">
|
||||
<h3 className="h4 fgColor-accent">
|
||||
<Link href={url}>{title}</Link>
|
||||
</h3>
|
||||
<div className="fgColor-muted mb-3 mt-2">{description}</div>
|
||||
|
||||
14
src/landings/components/ProductArticlesList.module.css
Normal file
14
src/landings/components/ProductArticlesList.module.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.linkItem {
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
span {
|
||||
color: var(--fgColor-accent, var(--color-accent-fg));
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import cx from 'classnames'
|
||||
|
||||
import { ActionList } from '@primer/react'
|
||||
|
||||
import { ProductTreeNode, useMainContext } from '@/frame/components/context/MainContext'
|
||||
import { Link } from '@/frame/components/Link'
|
||||
import clsx from 'clsx'
|
||||
import styles from './ProductArticlesList.module.css'
|
||||
|
||||
export const ProductArticlesList = () => {
|
||||
const { currentProductTree } = useMainContext()
|
||||
@@ -35,28 +35,19 @@ const ProductTreeNodeList = ({ treeNode }: { treeNode: ProductTreeNode }) => {
|
||||
<ActionList variant="full">
|
||||
{treeNode.childPages.map((childNode, index) => {
|
||||
return (
|
||||
<ActionList.Item
|
||||
as="li"
|
||||
<ActionList.LinkItem
|
||||
as="a"
|
||||
key={childNode.href + index}
|
||||
className={cx('width-full pl-0')}
|
||||
sx={{
|
||||
borderRadius: 0,
|
||||
':hover': {
|
||||
borderRadius: 0,
|
||||
},
|
||||
}}
|
||||
tabIndex={undefined}
|
||||
aria-labelledby={undefined}
|
||||
href={childNode.href}
|
||||
className={clsx(styles.linkItem, 'width-full', 'pl-0', 'd-block')}
|
||||
>
|
||||
<Link className="d-block width-full text-underline" href={childNode.href}>
|
||||
{childNode.title}
|
||||
{childNode.childPages.length > 0 ? (
|
||||
<small className="color-fg-muted d-inline-block">
|
||||
• {childNode.childPages.length} articles
|
||||
</small>
|
||||
) : null}
|
||||
</Link>
|
||||
</ActionList.Item>
|
||||
{childNode.title}
|
||||
{childNode.childPages.length > 0 ? (
|
||||
<small className="color-fg-muted d-inline-block">
|
||||
• {childNode.childPages.length} articles
|
||||
</small>
|
||||
) : null}
|
||||
</ActionList.LinkItem>
|
||||
)
|
||||
})}
|
||||
</ActionList>
|
||||
|
||||
3
src/landings/components/SidebarProduct.module.css
Normal file
3
src/landings/components/SidebarProduct.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.sidebar * {
|
||||
font-size: var(--h5-size, 14px) !important;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { ProductTreeNode, useMainContext } from '@/frame/components/context/Main
|
||||
import { useAutomatedPageContext } from '@/automated-pipelines/components/AutomatedPageContext'
|
||||
import { nonAutomatedRestPaths } from '@/rest/lib/config'
|
||||
|
||||
import styles from './SidebarProduct.module.css'
|
||||
|
||||
export const SidebarProduct = () => {
|
||||
const router = useRouter()
|
||||
const {
|
||||
@@ -32,7 +34,7 @@ export const SidebarProduct = () => {
|
||||
}
|
||||
|
||||
const productSection = () => (
|
||||
<div className="ml-3" data-testid="product-sidebar">
|
||||
<div data-testid="product-sidebar">
|
||||
<NavList aria-label="Product sidebar" role="navigation">
|
||||
{sidebarTree &&
|
||||
sidebarTree.childPages.map((childPage) => (
|
||||
@@ -50,7 +52,7 @@ export const SidebarProduct = () => {
|
||||
nonAutomatedRestPaths.every((item: string) => !page.href.includes(item)),
|
||||
)
|
||||
return (
|
||||
<div className="ml-3">
|
||||
<div>
|
||||
<NavList aria-label="REST sidebar overview articles" role="navigation">
|
||||
{conceptualPages.map((childPage) => (
|
||||
<NavListItem key={childPage.href} childPage={childPage} />
|
||||
@@ -69,7 +71,7 @@ export const SidebarProduct = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="sidebar" style={{ overflowY: 'auto' }} className="pt-3">
|
||||
<div data-testid="sidebar" className={styles.sidebar}>
|
||||
{isRestPage ? restSection() : productSection()}
|
||||
</div>
|
||||
)
|
||||
@@ -90,7 +92,7 @@ function NavListItem({ childPage }: { childPage: ProductTreeNode }) {
|
||||
>
|
||||
{childPage.title}
|
||||
{childPage.childPages.length > 0 && (
|
||||
<NavList.SubNav aria-label={`${childPage.title} submenu`} sx={{ '*': { fontSize: 1 } }}>
|
||||
<NavList.SubNav aria-label={`${childPage.title} submenu`}>
|
||||
{childPage.sidebarLink && (
|
||||
<NavList.Item
|
||||
href={childPage.sidebarLink.href}
|
||||
@@ -166,7 +168,7 @@ function RestNavListItem({ category }: { category: ProductTreeNode }) {
|
||||
>
|
||||
{category.title}
|
||||
{category.childPages.length > 0 && (
|
||||
<NavList.SubNav aria-label={`${category.title} submenu`} sx={{ '*': { fontSize: 1 } }}>
|
||||
<NavList.SubNav aria-label={`${category.title} submenu`}>
|
||||
{category.childPages.map((childPage) => {
|
||||
return (
|
||||
<NavList.Item
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import { ActionList } from '@primer/react'
|
||||
import { Link } from '@/frame/components/Link'
|
||||
import type { TocItem } from '@/landings/types'
|
||||
import { ActionList } from '@primer/react'
|
||||
|
||||
type Props = {
|
||||
items: Array<TocItem>
|
||||
@@ -45,9 +45,13 @@ export const TableOfContents = (props: Props) => {
|
||||
const { fullPath, title, childTocItems } = item
|
||||
return (
|
||||
<React.Fragment key={fullPath}>
|
||||
<ActionList.Item className="f4 color-fg-accent d-list-item d-block width-full text-underline">
|
||||
<Link href={fullPath}>{title}</Link>
|
||||
</ActionList.Item>
|
||||
<ActionList.LinkItem
|
||||
href={fullPath}
|
||||
as="a"
|
||||
className="f4 color-fg-accent d-list-item d-block width-full text-underline"
|
||||
>
|
||||
{title}
|
||||
</ActionList.LinkItem>
|
||||
{(childTocItems || []).length > 0 && (
|
||||
<li className="f4 color-fg-accent d-list-item d-block width-full text-underline">
|
||||
<ActionList
|
||||
@@ -57,12 +61,14 @@ export const TableOfContents = (props: Props) => {
|
||||
>
|
||||
{(childTocItems || []).filter(Boolean).map((childItem) => {
|
||||
return (
|
||||
<ActionList.Item
|
||||
<ActionList.LinkItem
|
||||
key={childItem.fullPath}
|
||||
href={childItem.fullPath}
|
||||
as="a"
|
||||
className="f4 color-fg-accent d-list-item d-block width-full text-underline"
|
||||
>
|
||||
<Link href={childItem.fullPath}>{childItem.title}</Link>
|
||||
</ActionList.Item>
|
||||
{childItem.title}
|
||||
</ActionList.LinkItem>
|
||||
)
|
||||
})}
|
||||
</ActionList>
|
||||
|
||||
@@ -8,7 +8,9 @@ describe('curated homepage links', () => {
|
||||
|
||||
test('English', async () => {
|
||||
const $ = await getDOM('/en')
|
||||
const $links = $('[data-testid=bump-link]')
|
||||
|
||||
// Update selector to find actual link elements within article list
|
||||
const $links = $('[data-testid=article-list] a')
|
||||
expect($links.length).toBeGreaterThanOrEqual(6)
|
||||
|
||||
// Check that each link is localized and includes a title and intro
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('featuredLinks', () => {
|
||||
vi.setConfig({ testTimeout: 60 * 1000 })
|
||||
|
||||
test('non-TOC pages do not have intro links', async () => {
|
||||
const $ = await getDOM('/en/get-started/start-your-journey')
|
||||
const $ = await getDOM('/en/get-started/start-your-journey/hello-world')
|
||||
expect($('[data-testid=article-list]')).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -16,19 +16,27 @@ describe('featuredLinks', () => {
|
||||
const $featuredLinks = $('[data-testid=article-list] a')
|
||||
expect($featuredLinks).toHaveLength(7)
|
||||
expect($featuredLinks.eq(0).attr('href')).toBe('/en/get-started/start-your-journey/hello-world')
|
||||
expect($featuredLinks.eq(0).children('h3').text()).toMatch('Hello World')
|
||||
expect($featuredLinks.eq(0).children('p').text()).toMatch('Follow this Hello World exercise')
|
||||
expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-title]').text()).toMatch(
|
||||
'Hello World',
|
||||
)
|
||||
expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-intro]').text()).toMatch(
|
||||
/follow.+this.+hello.+world.+exercise/i,
|
||||
)
|
||||
})
|
||||
|
||||
test('Enterprise intro links have expected values', async () => {
|
||||
const $ = await getDOM('/enterprise-server@latest/get-started')
|
||||
const $ = await getDOM('/en/enterprise-server@latest/get-started')
|
||||
const $featuredLinks = $('[data-testid=article-list] a')
|
||||
expect($featuredLinks.length).toBeGreaterThan(0)
|
||||
|
||||
// Fixture content expectations (CI environment)
|
||||
expect($featuredLinks.eq(0).attr('href')).toBe(
|
||||
`/en/enterprise-server@${enterpriseServerReleases.latestStable}/get-started/foo/bar`,
|
||||
)
|
||||
expect($featuredLinks.eq(0).children('h3').text()).toMatch('Bar Usually Comes After Foo')
|
||||
expect($featuredLinks.eq(0).children('p').text()).toMatch(
|
||||
expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-title]').text()).toMatch(
|
||||
'Bar Usually Comes After Foo',
|
||||
)
|
||||
expect($featuredLinks.eq(0).find('[data-testid=link-with-intro-intro]').text()).toMatch(
|
||||
"This page doesn't really have an intro",
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { GlobeIcon } from '@primer/octicons-react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useLanguages } from '@/languages/components/LanguagesContext'
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
import { useUserLanguage } from '@/languages/components/useUserLanguage'
|
||||
import { ActionList, ActionMenu, IconButton, Link } from '@primer/react'
|
||||
import { ActionList, ActionMenu, IconButton } from '@primer/react'
|
||||
|
||||
type Props = {
|
||||
xs?: boolean
|
||||
@@ -38,16 +38,16 @@ export const LanguagePicker = ({ xs, mediumOrLower }: Props) => {
|
||||
// in a "denormalized" way.
|
||||
const routerPath = router.asPath.split('#')[0]
|
||||
|
||||
// languageList is specifically <ActionList.Item>'s which are reused
|
||||
// languageList is specifically ActionList items which are reused
|
||||
// for menus that behave differently at the breakpoints.
|
||||
const languageList = langs.map((lang) => (
|
||||
<ActionList.Item
|
||||
<ActionList.LinkItem
|
||||
key={`/${lang.code}${routerPath}`}
|
||||
selected={lang === selectedLang}
|
||||
as={Link}
|
||||
as="a"
|
||||
active={lang === selectedLang}
|
||||
lang={lang.code}
|
||||
href={`/${lang.code}${routerPath}`}
|
||||
onSelect={() => {
|
||||
onClick={() => {
|
||||
if (lang.code) {
|
||||
try {
|
||||
setUserLanguageCookie(lang.code)
|
||||
@@ -63,7 +63,7 @@ export const LanguagePicker = ({ xs, mediumOrLower }: Props) => {
|
||||
}}
|
||||
>
|
||||
{lang.nativeName || lang.name}
|
||||
</ActionList.Item>
|
||||
</ActionList.LinkItem>
|
||||
))
|
||||
|
||||
// At large breakpoints, we return the full <ActionMenu> with just the languages,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import fs from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import frontmatter from '@/frame/lib/read-frontmatter'
|
||||
import getApplicableVersions from '@/versions/lib/get-applicable-versions'
|
||||
@@ -40,7 +40,7 @@ export interface ProductMap {
|
||||
|
||||
// Both internal and external products are specified in content/index.md
|
||||
const homepage = path.posix.join(ROOT, 'content/index.md')
|
||||
export const { data } = frontmatter(await fs.readFile(homepage, 'utf8'))
|
||||
export const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
|
||||
|
||||
export const productIds: string[] = data?.children || []
|
||||
|
||||
@@ -53,13 +53,13 @@ for (const productId of productIds) {
|
||||
|
||||
// Early Access may not exist in the current checkout
|
||||
try {
|
||||
await fs.readdir(dir)
|
||||
fs.readdirSync(dir)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
const toc = path.posix.join(dir, 'index.md')
|
||||
const fileContent = await fs.readFile(toc, 'utf8')
|
||||
const fileContent = fs.readFileSync(toc, 'utf8')
|
||||
const { data: tocData } = frontmatter(fileContent)
|
||||
if (tocData) {
|
||||
const applicableVersions = getApplicableVersions(tocData.versions, toc)
|
||||
|
||||
@@ -79,31 +79,29 @@ export const ApiVersionPicker = () => {
|
||||
|
||||
// This only shows the REST Version picker if it's calendar date versioned
|
||||
return allVersions[currentVersion].apiVersions.length > 0 ? (
|
||||
<div className="mb-3">
|
||||
<div data-testid="api-version-picker">
|
||||
<Picker
|
||||
defaultText={currentDateDisplayText}
|
||||
items={apiVersionLinks}
|
||||
pickerLabel="API Version: "
|
||||
alignment="start"
|
||||
buttonBorder={true}
|
||||
dataTestId="version"
|
||||
ariaLabel="Select API Version"
|
||||
onSelect={(item) => {
|
||||
if (item.extra?.currentDate) rememberApiVersion(item.extra.currentDate)
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
return item.extra?.info ? (
|
||||
<div className="f6">
|
||||
{item.text}
|
||||
<InfoIcon verticalAlign="middle" size={15} className="ml-1" />
|
||||
</div>
|
||||
) : (
|
||||
item.text
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="api-version-picker">
|
||||
<Picker
|
||||
defaultText={currentDateDisplayText}
|
||||
items={apiVersionLinks}
|
||||
pickerLabel="API Version: "
|
||||
alignment="start"
|
||||
buttonBorder={true}
|
||||
dataTestId="version"
|
||||
ariaLabel="Select API Version"
|
||||
onSelect={(item) => {
|
||||
if (item.extra?.currentDate) rememberApiVersion(item.extra.currentDate)
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
return item.extra?.info ? (
|
||||
<div className="f6">
|
||||
{item.text}
|
||||
<InfoIcon verticalAlign="middle" size={15} className="ml-1" />
|
||||
</div>
|
||||
) : (
|
||||
item.text
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -31,3 +31,13 @@
|
||||
.responseCodeBlock {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.segmentedControl {
|
||||
padding: 0 !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
|
||||
li + li {
|
||||
// Same as the Primer CSS selector for segmented control
|
||||
margin-top: -1px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef, FormEvent } from 'react'
|
||||
import { FormControl, IconButton, Select, TabNav } from '@primer/react'
|
||||
import { FormControl, IconButton, Select, SegmentedControl } from '@primer/react'
|
||||
import { CheckIcon, CopyIcon, InfoIcon } from '@primer/octicons-react'
|
||||
import { announce } from '@primer/live-region-element'
|
||||
import Cookies from '@/frame/components/lib/cookies'
|
||||
@@ -260,26 +260,28 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
|
||||
</div>
|
||||
<div className="border-top d-inline-flex flex-justify-between width-full flex-items-center pt-2">
|
||||
<div className="d-inline-flex ml-2">
|
||||
<TabNav aria-label={`Example language selector for ${operation.title}`}>
|
||||
<SegmentedControl
|
||||
className={styles.segmentedControl}
|
||||
aria-label={`Example language selector for ${operation.title}`}
|
||||
>
|
||||
{languageSelectOptions.map((optionKey) => (
|
||||
<TabNav.Link
|
||||
<SegmentedControl.Button
|
||||
key={optionKey}
|
||||
selected={optionKey === selectedLanguage}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
handleLanguageSelection(optionKey)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
onKeyDown={(event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleLanguageSelection(optionKey)
|
||||
}
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{t(`code_sample_options.${optionKey}`)}
|
||||
</TabNav.Link>
|
||||
</SegmentedControl.Button>
|
||||
))}
|
||||
</TabNav>
|
||||
</SegmentedControl>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<IconButton
|
||||
@@ -316,31 +318,30 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
|
||||
__html: displayedExample.response.description || t('response'),
|
||||
}}
|
||||
></h4>
|
||||
<div className="border rounded-1">
|
||||
<div className="border rounded-1 pt-2">
|
||||
{displayedExample.response.schema ? (
|
||||
<TabNav
|
||||
className="pt-2 mx-2"
|
||||
<SegmentedControl
|
||||
className={cx(styles.segmentedControl, 'mx-2')}
|
||||
aria-label={`Example response format selector for ${operation.title}`}
|
||||
>
|
||||
{responseSelectOptions.map((optionKey) => (
|
||||
<TabNav.Link
|
||||
<SegmentedControl.Button
|
||||
key={optionKey}
|
||||
selected={optionKey === selectedResponse}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
handleResponseSelection(optionKey)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
onKeyDown={(event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleResponseSelection(optionKey)
|
||||
}
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{t(`response_options.${optionKey}`)}
|
||||
</TabNav.Link>
|
||||
</SegmentedControl.Button>
|
||||
))}
|
||||
</TabNav>
|
||||
</SegmentedControl>
|
||||
) : null}
|
||||
<div className="">
|
||||
{/* Status code */}
|
||||
|
||||
@@ -30,6 +30,7 @@ $mutedTextColor: var(--fgColor-muted, var(--color-fg-muted, #656d76));
|
||||
ul,
|
||||
ol {
|
||||
list-style-type: decimal !important;
|
||||
padding-inline-start: 2em !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ $mutedTextColor: var(--fgColor-muted, var(--color-fg-muted, #656d76));
|
||||
}
|
||||
|
||||
.referencesList {
|
||||
padding-left: 16px !important;
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
|
||||
@@ -500,9 +500,6 @@ export function AskAIResults({
|
||||
const refIndex = index + referencesIndexOffset
|
||||
return (
|
||||
<ActionList.Item
|
||||
sx={{
|
||||
marginLeft: '0px',
|
||||
}}
|
||||
key={`reference-${index}`}
|
||||
id={`search-option-reference-${index + referencesIndexOffset}`}
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -111,10 +111,11 @@ $mutedTextColor: var(--fgColor-muted, var(--color-fg-muted, #656d76));
|
||||
}
|
||||
|
||||
.viewAllSearchResults {
|
||||
color: var(--color-accent-emphasis) !important;
|
||||
padding-left: 32px !important;
|
||||
span {
|
||||
font-weight: 500 !important;
|
||||
button {
|
||||
span {
|
||||
color: var(--color-accent-emphasis) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
executeGeneralSearch,
|
||||
GENERAL_SEARCH_CONTEXT,
|
||||
} from '../helpers/execute-search-actions'
|
||||
import { Banner } from '@primer/react/drafts'
|
||||
import { Banner } from '@primer/react/experimental'
|
||||
import { useCombinedSearchResults } from '@/search/components/hooks/useAISearchAutocomplete'
|
||||
import { AskAIResults } from './AskAIResults'
|
||||
import { sendEvent, uuidv4 } from '@/events/components/events'
|
||||
@@ -948,8 +948,11 @@ function renderSearchGroups(
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!option.isViewAllResults && !option.isNoResultsFound && (
|
||||
<ActionList.LeadingVisual aria-hidden>
|
||||
{!option.isNoResultsFound && (
|
||||
<ActionList.LeadingVisual
|
||||
aria-hidden
|
||||
sx={{ visibility: option.isViewAllResults ? 'hidden' : 'visible' }}
|
||||
>
|
||||
<FileIcon />
|
||||
</ActionList.LeadingVisual>
|
||||
)}
|
||||
|
||||
3
src/search/components/results/Aggregations.module.scss
Normal file
3
src/search/components/results/Aggregations.module.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.aggregations label {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
|
||||
import type { SearchResultAggregations } from '@/search/types'
|
||||
import styles from './Aggregations.module.scss'
|
||||
|
||||
type Props = {
|
||||
aggregations: SearchResultAggregations
|
||||
@@ -46,7 +47,7 @@ export function SearchResultsAggregations({ aggregations }: Props) {
|
||||
|
||||
if (aggregations.toplevel && aggregations.toplevel.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.aggregations}>
|
||||
<CheckboxGroup>
|
||||
<CheckboxGroup.Label>
|
||||
{t('filter')}{' '}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ActionList } from '@primer/react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { PickerItem } from './Picker'
|
||||
import { Link } from '@/frame/components/Link'
|
||||
|
||||
import styles from './Fields.module.scss'
|
||||
|
||||
@@ -21,11 +20,11 @@ export const Fields = (fieldProps: {
|
||||
item.divider ? (
|
||||
<ActionList.Divider key={`divider${i}`} />
|
||||
) : (
|
||||
<ActionList.Item
|
||||
as={Link}
|
||||
<ActionList.LinkItem
|
||||
as="a"
|
||||
key={item.text}
|
||||
href={item.href}
|
||||
selected={item.selected === true}
|
||||
active={item.selected === true}
|
||||
onSelect={() => {
|
||||
if (onSelect) onSelect(item)
|
||||
setOpen(!open)
|
||||
@@ -45,7 +44,7 @@ export const Fields = (fieldProps: {
|
||||
role={item.extra?.arrow || item.extra?.info ? 'menuitem' : 'menuitemradio'}
|
||||
>
|
||||
{renderItem ? renderItem(item) : item.text}
|
||||
</ActionList.Item>
|
||||
</ActionList.LinkItem>
|
||||
),
|
||||
)}
|
||||
</ActionList>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import semver from 'semver'
|
||||
|
||||
import versionSatisfiesRange from './version-satisfies-range'
|
||||
|
||||
const rawDates = JSON.parse(await fs.readFile('src/ghes-releases/lib/enterprise-dates.json'))
|
||||
const rawDates = JSON.parse(fs.readFileSync('src/ghes-releases/lib/enterprise-dates.json', 'utf8'))
|
||||
|
||||
// ============================================================================
|
||||
// STATICALLY DEFINED VALUES
|
||||
|
||||
Reference in New Issue
Block a user