From c3944238ed463c52b49c4c52ca10f8b8b3d67d8e Mon Sep 17 00:00:00 2001 From: Evan Bonsignori Date: Thu, 5 Jan 2023 12:59:23 -0800 Subject: [PATCH] add learning track card (#33300) Co-authored-by: Peter Bengtsson --- components/article/ArticlePage.tsx | 6 ++- components/article/LearningTrackCard.tsx | 56 ++++++++++++++++++++++++ components/article/LearningTrackNav.tsx | 26 ++++++----- components/context/ArticleContext.tsx | 7 ++- components/context/TocLandingContext.tsx | 7 +-- data/ui.yml | 6 ++- middleware/learning-track.js | 9 +++- tests/rendering/learning-tracks.js | 36 ++++++++++++++- 8 files changed, 129 insertions(+), 24 deletions(-) create mode 100644 components/article/LearningTrackCard.tsx diff --git a/components/article/ArticlePage.tsx b/components/article/ArticlePage.tsx index 714353c65bb2..40cfa61920e4 100644 --- a/components/article/ArticlePage.tsx +++ b/components/article/ArticlePage.tsx @@ -18,6 +18,7 @@ import { PlatformPicker } from 'components/article/PlatformPicker' import { ToolPicker } from 'components/article/ToolPicker' import { MiniTocs } from 'components/ui/MiniTocs' import { ClientSideHighlight } from 'components/ClientSideHighlight' +import { LearningTrackCard } from 'components/article/LearningTrackCard' import { RestRedirect } from 'components/RestRedirect' const ClientSideRefresh = dynamic(() => import('components/ClientSideRefresh'), { @@ -63,6 +64,8 @@ export const ArticlePage = () => { const { t } = useTranslation('pages') const currentPath = router.asPath.split('?')[0] + const isLearningPath = !!currentLearningTrack?.trackName + return ( {isDev && } @@ -114,6 +117,7 @@ export const ArticlePage = () => { )} + {isLearningPath && } {miniTocItems.length > 1 && } } @@ -131,7 +135,7 @@ export const ArticlePage = () => { - {currentLearningTrack?.trackName ? ( + {isLearningPath ? (
diff --git a/components/article/LearningTrackCard.tsx b/components/article/LearningTrackCard.tsx new file mode 100644 index 000000000000..3feb4c4e607c --- /dev/null +++ b/components/article/LearningTrackCard.tsx @@ -0,0 +1,56 @@ +import { useRouter } from 'next/router' + +import { Link } from 'components/Link' +import type { LearningTrack } from 'components/context/ArticleContext' +import { useTranslation } from 'components/hooks/useTranslation' + +type Props = { + track: LearningTrack +} +export function LearningTrackCard({ track }: Props) { + const { locale } = useRouter() + const { t } = useTranslation('learning_track_nav') + const { trackTitle, trackName, nextGuide, trackProduct, numberOfGuides, currentGuideIndex } = + track + return ( +
+
+ + {trackTitle} + + + {t('current_progress') + .replace('{n}', numberOfGuides) + .replace('{i}', currentGuideIndex + 1)} + +
+ + {nextGuide ? ( + <> + {t('next_guide')}: + + {nextGuide.title} + + + ) : ( + + {t('more_guides')} + + )} + +
+
+ ) +} diff --git a/components/article/LearningTrackNav.tsx b/components/article/LearningTrackNav.tsx index 49eb63f21502..1dad81e57878 100644 --- a/components/article/LearningTrackNav.tsx +++ b/components/article/LearningTrackNav.tsx @@ -1,3 +1,4 @@ +import { Link } from 'components/Link' import type { LearningTrack } from 'components/context/ArticleContext' import { useTranslation } from 'components/hooks/useTranslation' @@ -12,30 +13,33 @@ export function LearningTrackNav({ track }: Props) { data-testid="learning-track-nav" className="py-3 px-4 rounded color-bg-default border d-flex flex-justify-between" > - + {prevGuide && ( <> - {t('prevGuide')} - {t('prev_guide')} + {prevGuide.title} - + )} - + {nextGuide && ( <> - {t('nextGuide')} - {t('next_guide')} + {nextGuide.title} - + )} diff --git a/components/context/ArticleContext.tsx b/components/context/ArticleContext.tsx index 1c6535da2136..145b837708d1 100644 --- a/components/context/ArticleContext.tsx +++ b/components/context/ArticleContext.tsx @@ -1,10 +1,13 @@ import { createContext, useContext } from 'react' export type LearningTrack = { - trackName?: string - trackProduct?: string + trackTitle: string + trackName: string + trackProduct: string prevGuide?: { href: string; title: string } nextGuide?: { href: string; title: string } + numberOfGuides: number + currentGuideIndex: number } export type MiniTocItem = { diff --git a/components/context/TocLandingContext.tsx b/components/context/TocLandingContext.tsx index e92b82e144f4..8fe21595ec44 100644 --- a/components/context/TocLandingContext.tsx +++ b/components/context/TocLandingContext.tsx @@ -1,13 +1,8 @@ import pick from 'lodash/pick' import { createContext, useContext } from 'react' +import { LearningTrack } from './ArticleContext' import { FeaturedLink, getFeaturedLinksFromReq } from './ProductLandingContext' -export type LearningTrack = { - trackName?: string - prevGuide?: { href: string; title: string } - nextGuide?: { href: string; title: string } -} - export type TocItem = { fullPath: string title: string diff --git a/data/ui.yml b/data/ui.yml index 29dec0fbef90..06b51ff34635 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -256,8 +256,10 @@ product_guides: how_to: How-to guide reference: Reference learning_track_nav: - prevGuide: Previous guide - nextGuide: Next guide + prev_guide: Previous + next_guide: Next + more_guides: More guides → + current_progress: '{i} of {n} in learning path' toggle_images: off: Images are off, click to show on: Images are on, click to hide diff --git a/middleware/learning-track.js b/middleware/learning-track.js index 11e058a08ee5..4243f7b06b87 100644 --- a/middleware/learning-track.js +++ b/middleware/learning-track.js @@ -31,7 +31,11 @@ export default async function learningTrack(req, res, next) { const track = allLearningTracks[trackProduct][trackName] if (!track) return noTrack() - const currentLearningTrack = { trackName, trackProduct } + // The trackTitle comes from a data .yml file and may use Liquid templating, so we need to render it + const renderOpts = { textOnly: true } + const trackTitle = await renderContent(track.title, req.context, renderOpts) + + const currentLearningTrack = { trackName, trackProduct, trackTitle } const guidePath = getPathWithoutLanguage(getPathWithoutVersion(req.pagePath)) // The raw track.guides will return all guide paths, need to use getLinkData @@ -64,6 +68,9 @@ export default async function learningTrack(req, res, next) { if (guideIndex < 0) return noTrack() + currentLearningTrack.numberOfGuides = trackGuidePaths.length + currentLearningTrack.currentGuideIndex = guideIndex + if (guideIndex > 0) { const prevGuidePath = trackGuidePaths[guideIndex - 1] const result = await getLinkData(prevGuidePath, req.context, { title: true, intro: false }) diff --git a/tests/rendering/learning-tracks.js b/tests/rendering/learning-tracks.js index d0511294a93f..f8f7319e5662 100644 --- a/tests/rendering/learning-tracks.js +++ b/tests/rendering/learning-tracks.js @@ -1,4 +1,4 @@ -import { jest } from '@jest/globals' +import { expect, jest } from '@jest/globals' import { getDOM } from '../helpers/e2etest.js' @@ -49,6 +49,40 @@ describe('navigation banner', () => { }) }) + test('render guides index page and verify that the first page has guide navigation links', async () => { + let $ = await getDOM('/en/code-security/guides') + + let firstLearningPathURL = null + $('a[href]').each((i, element) => { + if (firstLearningPathURL) { + return + } + const href = $(element).attr('href') + if (new URLSearchParams(href.split('?')[1]).has('learn')) { + firstLearningPathURL = href + } + }) + + expect(firstLearningPathURL).not.toBeNull() + + // Navigate to the first learning path URL + $ = await getDOM(firstLearningPathURL) + + expect($('[data-testid=learning-track-card]')).toHaveLength(1) + const $navLinks = $('[data-testid=learning-track-card] a') + expect($navLinks).toHaveLength(2) + $navLinks.each((i, elem) => { + // First link is to the guides index page + if (i === 0) { + expect($(elem).attr('href')).toEqual(expect.stringContaining('/guides')) + // Other links are to the next page in the learning track + } else { + expect($(elem).attr('href')).toMatch('learn=security_advisories') + } + }) + expect.assertions(1 + 2 + 2) + }) + test('render navigation banner when url is a redirect to a learning track URL', async () => { const $ = await getDOM( '/en/enterprise/admin/enterprise-management/enabling-automatic-update-checks?learn=upgrade_your_instance'