Create & migrate a subject folder for learning-tracks (#37890)
Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { DefaultLayout } from 'components/DefaultLayout'
|
||||
import { useProductGuidesContext } from 'src/landings/components/ProductGuidesContext'
|
||||
import { LandingSection } from 'src/landings/components/LandingSection'
|
||||
import { GuidesHero } from 'src/landings/components/GuidesHero'
|
||||
import { LearningTracks } from 'src/landings/components/LearningTracks'
|
||||
import { LearningTracks } from 'src/learning-track/components/guides/LearningTracks'
|
||||
import { ArticleCards } from 'src/landings/components/ArticleCards'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ArticleList } from 'src/landings/components/ArticleList'
|
||||
import { ArticleGridLayout } from 'components/article/ArticleGridLayout'
|
||||
import { Callout } from 'components/ui/Callout'
|
||||
import { Lead } from 'components/ui/Lead'
|
||||
import { LearningTrackNav } from 'components/article/LearningTrackNav'
|
||||
import { LearningTrackNav } from 'src/learning-track/components/article/LearningTrackNav'
|
||||
import { ClientSideRedirects } from 'src/rest/components/ClientSideRedirects'
|
||||
import { RestRedirect } from 'src/rest/components/RestRedirect'
|
||||
import { Breadcrumbs } from 'components/page-header/Breadcrumbs'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import getLinkData from '../../../lib/get-link-data.js'
|
||||
import getLinkData from '#src/learning-track/lib/get-link-data.js'
|
||||
import renderContent from '../../../lib/render-content/index.js'
|
||||
|
||||
// this middleware adds properties to the context object
|
||||
|
||||
56
src/learning-track/components/article/LearningTrackCard.tsx
Normal file
56
src/learning-track/components/article/LearningTrackCard.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
data-testid="learning-track-card"
|
||||
className="py-3 px-4 rounded color-bg-default border d-flex flex-justify-between mb-4 mx-2"
|
||||
>
|
||||
<div className="d-flex flex-column width-full">
|
||||
<Link href={`/${locale}/${trackProduct}/guides`} className="h4 color-fg-default mb-1">
|
||||
{trackTitle}
|
||||
</Link>
|
||||
<span className="f5 color-fg-muted">
|
||||
{t('current_progress')
|
||||
.replace('{n}', numberOfGuides)
|
||||
.replace('{i}', currentGuideIndex + 1)}
|
||||
</span>
|
||||
<hr />
|
||||
<span className="h5 color-fg-default">
|
||||
{nextGuide ? (
|
||||
<>
|
||||
{t('next_guide')}:
|
||||
<Link
|
||||
href={`${nextGuide.href}?${new URLSearchParams({
|
||||
learn: trackName,
|
||||
learnProduct: trackProduct,
|
||||
})}`}
|
||||
className="text-bold color-fg f5 ml-1"
|
||||
>
|
||||
{nextGuide.title}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${locale}/${trackProduct}/guides`}
|
||||
className="h5 text-bold color-fg f5 ml-1"
|
||||
>
|
||||
{t('more_guides')}
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
src/learning-track/components/article/LearningTrackNav.tsx
Normal file
48
src/learning-track/components/article/LearningTrackNav.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Link } from 'components/Link'
|
||||
import type { LearningTrack } from 'components/context/ArticleContext'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
type Props = {
|
||||
track: LearningTrack
|
||||
}
|
||||
export function LearningTrackNav({ track }: Props) {
|
||||
const { t } = useTranslation('learning_track_nav')
|
||||
const { prevGuide, nextGuide, trackName, trackProduct } = track
|
||||
return (
|
||||
<div
|
||||
data-testid="learning-track-nav"
|
||||
className="py-3 px-4 rounded color-bg-default border d-flex flex-justify-between"
|
||||
>
|
||||
<span className="f5 d-flex flex-column">
|
||||
{prevGuide && (
|
||||
<>
|
||||
<span className="color-fg-default">{t('prev_guide')}</span>
|
||||
<Link
|
||||
href={`${prevGuide.href}?learn=${trackName}&learnProduct=${trackProduct}`}
|
||||
className="text-bold color-fg"
|
||||
>
|
||||
{prevGuide.title}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="f5 d-flex flex-column flex-items-end">
|
||||
{nextGuide && (
|
||||
<>
|
||||
<span className="color-fg-default">{t('next_guide')}</span>
|
||||
<Link
|
||||
href={`${nextGuide.href}?${new URLSearchParams({
|
||||
learn: trackName,
|
||||
learnProduct: trackProduct,
|
||||
})}`}
|
||||
className="text-bold color-fg text-right"
|
||||
>
|
||||
{nextGuide.title}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useProductGuidesContext } from 'src/landings/components/ProductGuidesContext'
|
||||
import { LearningTrack } from 'src/landings/components/LearningTrack'
|
||||
import { LearningTrack } from 'src/learning-track/components/guides/LearningTrack'
|
||||
|
||||
export const LearningTracks = () => {
|
||||
const { learningTracks } = useProductGuidesContext()
|
||||
55
src/learning-track/lib/get-link-data.js
Normal file
55
src/learning-track/lib/get-link-data.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import path from 'path'
|
||||
import findPage from '../../../lib/find-page.js'
|
||||
import nonEnterpriseDefaultVersion from '../../../lib/non-enterprise-default-version.js'
|
||||
import removeFPTFromPath from '../../../lib/remove-fpt-from-path.js'
|
||||
import renderContent from '../../../lib/render-content/index.js'
|
||||
|
||||
// rawLinks is an array of paths: [ '/foo' ]
|
||||
// we need to convert it to an array of localized objects: [ { href: '/en/foo', title: 'Foo', intro: 'Description here' } ]
|
||||
export default async (
|
||||
rawLinks,
|
||||
context,
|
||||
option = { title: true, intro: true, fullTitle: false }
|
||||
) => {
|
||||
if (!rawLinks) return
|
||||
|
||||
if (typeof rawLinks === 'string') {
|
||||
return await processLink(rawLinks, context, option)
|
||||
}
|
||||
|
||||
const links = (
|
||||
await Promise.all(rawLinks.map((link) => processLink(link, context, option)))
|
||||
).filter(Boolean)
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
async function processLink(link, context, option) {
|
||||
const opts = { textOnly: true }
|
||||
// Parse the link in case it includes Liquid conditionals
|
||||
const linkPath = await renderContent(link.href || link, context, opts)
|
||||
if (!linkPath) return null
|
||||
|
||||
const version =
|
||||
context.currentVersion === 'homepage' ? nonEnterpriseDefaultVersion : context.currentVersion
|
||||
const href = removeFPTFromPath(path.join('/', context.currentLanguage, version, linkPath))
|
||||
|
||||
const linkedPage = findPage(href, context.pages, context.redirects)
|
||||
if (!linkedPage) return null
|
||||
|
||||
const result = { href, page: linkedPage }
|
||||
|
||||
if (option.title) {
|
||||
result.title = await linkedPage.renderTitle(context, opts)
|
||||
}
|
||||
|
||||
if (option.fullTitle) {
|
||||
opts.preferShort = false
|
||||
result.fullTitle = await linkedPage.renderTitle(context, opts)
|
||||
}
|
||||
|
||||
if (option.intro) {
|
||||
result.intro = await linkedPage.renderProp('intro', context, opts)
|
||||
}
|
||||
return result
|
||||
}
|
||||
126
src/learning-track/lib/process-learning-tracks.js
Normal file
126
src/learning-track/lib/process-learning-tracks.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import renderContent from '../../../lib/render-content/index.js'
|
||||
import getLinkData from './get-link-data.js'
|
||||
import getApplicableVersions from '../../../lib/get-applicable-versions.js'
|
||||
import { getDataByLanguage } from '../../../lib/get-data.js'
|
||||
import { executeWithFallback } from '../../../lib/render-with-fallback.js'
|
||||
|
||||
const renderOpts = { textOnly: true }
|
||||
|
||||
// This module returns an object that contains a single featured learning track
|
||||
// and an array of all the other learning tracks for the current version.
|
||||
export default async function processLearningTracks(rawLearningTracks, context) {
|
||||
const learningTracks = []
|
||||
|
||||
let featuredTrack
|
||||
|
||||
if (!context.currentProduct) {
|
||||
throw new Error(`Missing context.currentProduct value.`)
|
||||
}
|
||||
|
||||
for (const rawTrackName of rawLearningTracks) {
|
||||
let isFeaturedTrack = false
|
||||
|
||||
// Track names in frontmatter may include Liquid conditionals.
|
||||
const renderedTrackName = await renderContent(rawTrackName, context, renderOpts)
|
||||
if (!renderedTrackName) continue
|
||||
|
||||
// Find the data for the current product and track name.
|
||||
|
||||
if (context.currentProduct.includes('.')) {
|
||||
throw new Error(`currentProduct can not contain a . (${context.currentProduct})`)
|
||||
}
|
||||
if (renderedTrackName.includes('.')) {
|
||||
throw new Error(`renderedTrackName can not contain a . (${renderedTrackName})`)
|
||||
}
|
||||
|
||||
// Note: this will use the translated learning tracks and automatically
|
||||
// fall back to English if they don't exist on disk in the translation.
|
||||
const track = getDataByLanguage(
|
||||
`learning-tracks.${context.currentProduct}.${renderedTrackName}`,
|
||||
context.currentLanguage
|
||||
)
|
||||
if (!track) {
|
||||
throw new Error(`No learning track called '${renderedTrackName}'.`)
|
||||
}
|
||||
|
||||
// If the current language isn't 'en' we need to prepare and have the
|
||||
// English quivalent ready.
|
||||
// We do this for two reasons:
|
||||
//
|
||||
// 1. For each learning-track .yml file (in data) always want the
|
||||
// English values for `guides`, `versions`, `featured_track`.
|
||||
// Meaning, for the translated learning tracks we only keep the
|
||||
// `title` and `description`.
|
||||
//
|
||||
// 2. When we attempt to render the translated learning tracks'
|
||||
// `title` and `description`, if they are failing to render,
|
||||
// we need to have the English `title` and `description` to
|
||||
// fall back to.
|
||||
//
|
||||
let enTrack
|
||||
if (context.currentLanguage !== 'en') {
|
||||
enTrack = getDataByLanguage(
|
||||
`learning-tracks.${context.currentProduct}.${renderedTrackName}`,
|
||||
'en'
|
||||
)
|
||||
// Sometimes the translations have more than just translated the
|
||||
// `title` and `description`, but also things that don't make sense
|
||||
// to translate like `guides` and `versions`. Always draw that
|
||||
// from the English equivalent.
|
||||
track.guides = enTrack.guides
|
||||
track.versions = enTrack.versions
|
||||
track.featured_track = enTrack.featured_track
|
||||
}
|
||||
|
||||
// If there is no `versions` prop in the learning track frontmatter, assume the track should display in all versions.
|
||||
if (track.versions) {
|
||||
const trackVersions = getApplicableVersions(track.versions)
|
||||
|
||||
// If the current version is not included, do not display the track.
|
||||
if (!trackVersions.includes(context.currentVersion)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const title = await executeWithFallback(
|
||||
context,
|
||||
() => renderContent(track.title, context, renderOpts),
|
||||
(enContext) => renderContent(enTrack.title, enContext, renderOpts)
|
||||
)
|
||||
const description = await executeWithFallback(
|
||||
context,
|
||||
() => renderContent(track.description, context, renderOpts),
|
||||
(enContext) => renderContent(enTrack.description, enContext, renderOpts)
|
||||
)
|
||||
|
||||
const learningTrack = {
|
||||
trackName: renderedTrackName,
|
||||
trackProduct: context.currentProduct || null,
|
||||
title,
|
||||
description,
|
||||
// getLinkData respects versioning and only returns guides available in the current version;
|
||||
// if no guides are available, the learningTrack.guides property will be an empty array.
|
||||
guides: await getLinkData(track.guides, context),
|
||||
}
|
||||
|
||||
// Determine if this is the featured track.
|
||||
if (track.featured_track) {
|
||||
// Featured track properties may be booleans or string that include Liquid conditionals with versioning.
|
||||
// We need to parse any strings to determine if the featured track is relevant for this version.
|
||||
isFeaturedTrack =
|
||||
track.featured_track === true ||
|
||||
(await renderContent(track.featured_track, context, renderOpts)) === 'true'
|
||||
|
||||
if (isFeaturedTrack) {
|
||||
featuredTrack = learningTrack
|
||||
}
|
||||
}
|
||||
|
||||
// Only add the track to the array of tracks if there are guides in this version and it's not the featured track.
|
||||
if (learningTrack.guides.length && !isFeaturedTrack) {
|
||||
learningTracks.push(learningTrack)
|
||||
}
|
||||
}
|
||||
|
||||
return { featuredTrack, learningTracks }
|
||||
}
|
||||
118
src/learning-track/middleware/learning-track.js
Normal file
118
src/learning-track/middleware/learning-track.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import { getPathWithoutLanguage, getPathWithoutVersion } from '../../../lib/path-utils.js'
|
||||
import getLinkData from '../lib/get-link-data.js'
|
||||
import renderContent from '../../../lib/render-content/renderContent.js'
|
||||
import { getDeepDataByLanguage } from '../../../lib/get-data.js'
|
||||
|
||||
export default async function learningTrack(req, res, next) {
|
||||
const noTrack = () => {
|
||||
req.context.currentLearningTrack = {}
|
||||
return next()
|
||||
}
|
||||
|
||||
if (!req.context.page) return next()
|
||||
|
||||
const trackName = req.query.learn
|
||||
if (!trackName) return noTrack()
|
||||
|
||||
let trackProduct = req.context.currentProduct
|
||||
const allLearningTracks = getDeepDataByLanguage('learning-tracks', req.language)
|
||||
let tracksPerProduct = allLearningTracks[trackProduct]
|
||||
|
||||
// If there are no learning tracks for the current product, try and fall
|
||||
// back to the learning track product set as a URL parameter. This handles
|
||||
// the case where a learning track has guide paths for a different product
|
||||
// than the current learning track product.
|
||||
if (!tracksPerProduct) {
|
||||
trackProduct = req.query.learnProduct
|
||||
tracksPerProduct = allLearningTracks[trackProduct]
|
||||
}
|
||||
if (!tracksPerProduct) return noTrack()
|
||||
|
||||
const track = allLearningTracks[trackProduct][trackName]
|
||||
if (!track) return noTrack()
|
||||
|
||||
// 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
|
||||
// so we only get guides available in the current version
|
||||
const trackGuides = await getLinkData(track.guides, req.context)
|
||||
|
||||
const trackGuidePaths = trackGuides.map((guide) => {
|
||||
return getPathWithoutLanguage(getPathWithoutVersion(guide.href))
|
||||
})
|
||||
|
||||
let guideIndex = trackGuidePaths.findIndex((path) => path === guidePath)
|
||||
|
||||
// The learning track path may use Liquid version conditionals, handle the
|
||||
// case where the requested path is a learning track path but won't match
|
||||
// because of a Liquid conditional.
|
||||
if (guideIndex < 0) {
|
||||
guideIndex = await indexOfLearningTrackGuide(trackGuidePaths, guidePath, req.context)
|
||||
}
|
||||
|
||||
// Also check if the learning track path is now a redirect to the requested
|
||||
// page, we still want to render the learning track banner in that case.
|
||||
// Also handles Liquid conditionals in the track path.
|
||||
if (guideIndex < 0) {
|
||||
for (const redirect of req.context.page.redirect_from || []) {
|
||||
if (guideIndex >= 0) break
|
||||
|
||||
guideIndex = await indexOfLearningTrackGuide(trackGuidePaths, redirect, req.context)
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
if (!result) return noTrack()
|
||||
|
||||
const href = result.href
|
||||
const title = result.title
|
||||
currentLearningTrack.prevGuide = { href, title }
|
||||
}
|
||||
|
||||
if (guideIndex < trackGuidePaths.length - 1) {
|
||||
const nextGuidePath = trackGuidePaths[guideIndex + 1]
|
||||
const result = await getLinkData(nextGuidePath, req.context, { title: true, intro: false })
|
||||
if (!result) return noTrack()
|
||||
|
||||
const href = result.href
|
||||
const title = result.title
|
||||
|
||||
currentLearningTrack.nextGuide = { href, title }
|
||||
}
|
||||
|
||||
req.context.currentLearningTrack = currentLearningTrack
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// Find the index of a learning track guide path in an array of guide paths,
|
||||
// return -1 if not found.
|
||||
async function indexOfLearningTrackGuide(trackGuidePaths, guidePath, context) {
|
||||
let guideIndex = -1
|
||||
|
||||
for (let i = 0; i < trackGuidePaths.length; i++) {
|
||||
// Learning track URLs may have Liquid conditionals.
|
||||
const renderedGuidePath = await renderContent(trackGuidePaths[i], context, { textOnly: true })
|
||||
|
||||
if (!renderedGuidePath) continue
|
||||
|
||||
if (renderedGuidePath === guidePath) {
|
||||
guideIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return guideIndex
|
||||
}
|
||||
Reference in New Issue
Block a user