Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Site Nav from tag graph #4439

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions adminSiteServer/mockSiteRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,15 @@ getPlainRouteWithROTransaction(
}
)

getPlainRouteWithROTransaction(
mockSiteRouter,
"/topicTagGraph.json",
async (req, res, trx) => {
const headerMenu = await db.generateTopicTagGraph(trx)
res.send(headerMenu)
}
)

getPlainRouteWithROTransaction(mockSiteRouter, "/*", async (req, res, trx) => {
// Remove leading and trailing slashes
const slug = req.path.replace(/^\/|\/$/g, "")
Expand Down
7 changes: 7 additions & 0 deletions baker/SiteBaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,13 @@ export class SiteBaker {
`rm -rf ${this.bakedSiteDir}/assets && cp -r ${BASE_DIR}/dist/assets ${this.bakedSiteDir}/assets`
)

await fs.writeFile(
`${this.bakedSiteDir}/topicTagGraph.json`,
await db
.generateTopicTagGraph(trx)
.then((nav) => JSON.stringify(nav))
)

// The `assets-admin` folder is optional; don't fail if it doesn't exist
await execWrapper(
`rm -rf ${this.bakedSiteDir}/assets-admin && (cp -r ${BASE_DIR}/dist/assets-admin ${this.bakedSiteDir}/assets-admin || true)`
Expand Down
42 changes: 42 additions & 0 deletions db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
MinimalExplorerInfo,
DbEnrichedImage,
DbEnrichedImageWithUserId,
TagGraphRoot,
} from "@ourworldindata/types"
import { groupBy, uniq } from "lodash"
import { gdocFromJSON } from "./model/Gdoc/GdocFactory.js"
Expand Down Expand Up @@ -504,6 +505,7 @@ export async function getFlatTagGraph(knex: KnexReadonlyTransaction): Promise<
tg.parentId,
tg.childId,
tg.weight,
t.slug,
t.name,
p.slug IS NOT NULL AS isTopic
FROM
Expand Down Expand Up @@ -806,3 +808,43 @@ export function getImageUsage(trx: KnexReadonlyTransaction): Promise<
)
)
}

export async function generateTopicTagGraph(
knex: KnexReadonlyTransaction
): Promise<TagGraphRoot> {
const { __rootId, ...parents } = await getFlatTagGraph(knex)

const tagGraphTopicsOnly = Object.entries(parents).reduce(
(acc: FlatTagGraph, [parentId, children]) => {
acc[Number(parentId)] = children.filter((child) => {
if (child.parentId === __rootId) return true
return child.isTopic
})
return acc
},
{} as FlatTagGraph
)

return createTagGraph(tagGraphTopicsOnly, __rootId)
}

export const getUniqueTopicCount = (
trx: KnexReadonlyTransaction
): Promise<number> => {
const count = knexRawFirst<{ count: number }>(
trx,
`-- sql
SELECT COUNT(DISTINCT(t.slug))
FROM tags t
LEFT JOIN posts_gdocs p ON t.slug = p.slug
WHERE t.slug IS NOT NULL AND p.published IS TRUE`
)
.then((res) => (res ? res.count : 0))
.catch((e) => {
console.error("Failed to get unique topic count", e)
throw e
})
// throw on count == 0 also
if (!count) throw new Error("Failed to get unique topic count")
return count
}
4 changes: 2 additions & 2 deletions db/model/Gdoc/GdocHomepage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
OwidGdocBaseInterface,
OwidGdocHomepageMetadata,
} from "@ourworldindata/types"
import { getUniqueTopicCount } from "../../../site/gdocs/utils.js"
import { getLatestDataInsights } from "./GdocFactory.js"

export class GdocHomepage
Expand Down Expand Up @@ -69,7 +68,8 @@ export class GdocHomepage

this.homepageMetadata = {
chartCount: grapherCount + nonGrapherExplorerViewCount,
topicCount: getUniqueTopicCount(),
topicCount: await db.getUniqueTopicCount(knex),
tagGraph: await db.generateTopicTagGraph(knex),
}

const { dataInsights, imageMetadata } =
Expand Down
19 changes: 2 additions & 17 deletions db/model/Post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
parsePostWpApiSnapshot,
FullPost,
JsonError,
CategoryWithEntries,
WP_PostType,
FilterFnPostRestApi,
PostRestApi,
Expand Down Expand Up @@ -40,7 +39,6 @@ import {
CLOUDFLARE_IMAGES_URL,
} from "../../settings/clientSettings.js"
import { BLOG_SLUG } from "../../settings/serverSettings.js"
import { SiteNavigationStatic } from "../../site/SiteConstants.js"
import { decodeHTML } from "entities"
import { getAndLoadListedGdocPosts } from "./Gdoc/GdocFactory.js"

Expand Down Expand Up @@ -185,22 +183,9 @@ export const getFullPostByIdFromSnapshot = async (
return getFullPost(trx, postEnriched.wpApiSnapshot)
}

// TODO: I suggest that in the place where we define SiteNavigationStatic we create a Set with all the leaves and
// then this one becomes a simple lookup in the set. Probably nicest to do the set creation as a memoized function.
// This function used to be more complicated, but now the only citable WP post is the COVID page
export const isPostSlugCitable = (slug: string): boolean => {
const entries = SiteNavigationStatic.categories
return entries.some((category) => {
return (
category.entries.some((entry) => entry.slug === slug) ||
(category.subcategories ?? []).some(
(subcategory: CategoryWithEntries) => {
return subcategory.entries.some(
(subCategoryEntry) => subCategoryEntry.slug === slug
)
}
)
)
})
return slug === "coronavirus"
}

export const getPostsFromSnapshots = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type FlatTagGraphNode = Pick<DbPlainTag, "name" | "slug"> & {
isTopic: boolean
parentId: number
childId: number
slug: string | null
}

export type FlatTagGraph = Record<number, FlatTagGraphNode[]>
Expand Down
2 changes: 2 additions & 0 deletions packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "./ArchieMlComponents.js"
import { MinimalTag } from "../dbTypes/Tags.js"
import { DbEnrichedLatestWork } from "../domainTypes/Author.js"
import { TagGraphRoot } from "../domainTypes/ContentGraph.js"

export enum OwidGdocPublicationContext {
unlisted = "unlisted",
Expand Down Expand Up @@ -178,6 +179,7 @@ export interface OwidGdocHomepageContent {
export interface OwidGdocHomepageMetadata {
chartCount?: number
topicCount?: number
tagGraph?: TagGraphRoot
}

export interface OwidGdocHomepageInterface extends OwidGdocBaseInterface {
Expand Down
7 changes: 7 additions & 0 deletions packages/@ourworldindata/utils/src/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,13 @@ export function createTagGraph(
return recursivelySetChildren(tagGraph) as TagGraphRoot
}

export const getAllTopicsInArea = (area: TagGraphNode): TagGraphNode[] => {
return [
...area.children,
...area.children.flatMap((child) => getAllTopicsInArea(child)),
]
}

export function formatInlineList(
array: unknown[],
connector: "and" | "or" = "and"
Expand Down
1 change: 1 addition & 0 deletions packages/@ourworldindata/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export {
commafyNumber,
isFiniteWithGuard,
createTagGraph,
getAllTopicsInArea,
formatInlineList,
lazy,
getParentVariableIdFromChartConfig,
Expand Down
31 changes: 12 additions & 19 deletions site/SiteMobileCategory.tsx → site/SiteMobileArea.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { useEffect, useRef } from "react"
import { CategoryWithEntries } from "@ourworldindata/utils"
import { SiteNavigationToggle } from "./SiteNavigationToggle.js"
import { SiteNavigationTopic } from "./SiteNavigationTopic.js"
import { allTopicsInCategory } from "./gdocs/utils.js"
import { TagGraphNode, getAllTopicsInArea } from "@ourworldindata/utils"
import { SiteNavigationToggle } from "./SiteNavigationToggle.js"

export const SiteMobileCategory = ({
category,
export const SiteMobileArea = ({
area,
isActive,
toggleCategory,
toggleArea,
}: {
category: CategoryWithEntries
area: TagGraphNode
isActive: boolean
toggleCategory: (category: CategoryWithEntries) => void
toggleArea: (category: TagGraphNode) => void
}) => {
const categoryRef = useRef<HTMLLIElement>(null)

Expand All @@ -22,20 +21,14 @@ export const SiteMobileCategory = ({
}, [isActive])

return (
<li
key={category.slug}
className="SiteMobileCategory"
ref={categoryRef}
>
<li key={area.slug} className="SiteMobileCategory" ref={categoryRef}>
<SiteNavigationToggle
ariaLabel={
isActive ? `Collapse ${category}` : `Expand ${category}`
}
ariaLabel={isActive ? `Collapse ${area}` : `Expand ${area}`}
isActive={isActive}
onToggle={() => toggleCategory(category)}
onToggle={() => toggleArea(area)}
dropdown={
<ul>
{allTopicsInCategory(category).map((topic) => (
{getAllTopicsInArea(area).map((topic) => (
<SiteNavigationTopic
key={topic.slug}
topic={topic}
Expand All @@ -45,7 +38,7 @@ export const SiteMobileCategory = ({
}
withCaret={true}
>
{category.name}
{area.name}
</SiteNavigationToggle>
</li>
)
Expand Down
31 changes: 15 additions & 16 deletions site/SiteMobileMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import { useState } from "react"
import { CategoryWithEntries } from "@ourworldindata/utils"
import { TagGraphNode, TagGraphRoot } from "@ourworldindata/utils"
import classnames from "classnames"
import { SiteNavigationToggle } from "./SiteNavigationToggle.js"
import { Menu } from "./SiteConstants.js"
import { SiteAbout } from "./SiteAbout.js"
import { SiteResources } from "./SiteResources.js"
import { SiteMobileCategory } from "./SiteMobileCategory.js"
import { SiteMobileArea } from "./SiteMobileArea.js"

export const SiteMobileMenu = ({
topics,
tagGraph,
menu,
toggleMenu,
className,
}: {
topics: CategoryWithEntries[]
tagGraph: TagGraphRoot | null
menu: Menu | null
toggleMenu: (menu: Menu) => void
className?: string
}) => {
const [activeCategory, setActiveCategory] =
useState<CategoryWithEntries | null>(null)
const [activeArea, setActiveArea] = useState<TagGraphNode | null>(null)

const toggleCategory = (category: CategoryWithEntries) => {
if (activeCategory === category) {
setActiveCategory(null)
const toggleArea = (area: TagGraphNode) => {
if (activeArea === area) {
setActiveArea(null)
} else {
setActiveCategory(category)
setActiveArea(area)
}
}

Expand All @@ -35,12 +34,12 @@ export const SiteMobileMenu = ({
<li>
<span className="section__header">Browse by topic</span>
<ul className="section__dropdown--topics">
{topics.map((category) => (
<SiteMobileCategory
key={category.slug}
category={category}
isActive={activeCategory === category}
toggleCategory={toggleCategory}
{tagGraph?.children.map((area) => (
<SiteMobileArea
key={area.slug}
area={area}
isActive={activeArea === area}
toggleArea={toggleArea}
/>
))}
</ul>
Expand Down
19 changes: 14 additions & 5 deletions site/SiteNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js"
import { SiteNavigationTopics } from "./SiteNavigationTopics.js"
import { SiteLogos } from "./SiteLogos.js"
import { SiteAbout } from "./SiteAbout.js"
import { TagGraphRoot } from "@ourworldindata/utils"
import { SiteResources } from "./SiteResources.js"
import { SiteSearchNavigation } from "./SiteSearchNavigation.js"
import { SiteMobileMenu } from "./SiteMobileMenu.js"
import { SiteNavigationToggle } from "./SiteNavigationToggle.js"
import classnames from "classnames"
import { useTriggerOnEscape } from "./hooks.js"
import { AUTOCOMPLETE_CONTAINER_ID } from "./search/Autocomplete.js"
import { Menu, SiteNavigationStatic } from "./SiteConstants.js"
import { Menu } from "./SiteConstants.js"

// Note: tranforming the flag from an env string to a boolean in
// clientSettings.ts is convoluted due to the two-pass SSR/Vite build process.
Expand All @@ -35,6 +36,16 @@ export const SiteNavigation = ({
}) => {
const [menu, setActiveMenu] = useState<Menu | null>(null)
const [query, setQuery] = useState<string>("")
const [tagGraph, setTagGraph] = useState<TagGraphRoot | null>(null)

useEffect(() => {
const fetchTagGraph = async () => {
const response = await fetch("/topicTagGraph.json")
const tagGraph = await response.json()
setTagGraph(tagGraph)
}
if (!tagGraph) fetchTagGraph().catch(console.error)
}, [tagGraph, setTagGraph])

const isActiveMobileMenu =
menu !== null &&
Expand Down Expand Up @@ -111,7 +122,7 @@ export const SiteNavigation = ({
<SiteMobileMenu
menu={menu}
toggleMenu={toggleMenu}
topics={SiteNavigationStatic.categories}
tagGraph={tagGraph}
className="hide-sm-up"
/>
}
Expand All @@ -131,9 +142,7 @@ export const SiteNavigation = ({
dropdown={
<SiteNavigationTopics
onClose={closeOverlay}
topics={
SiteNavigationStatic.categories
}
tagGraph={tagGraph}
className="hide-sm-only"
/>
}
Expand Down
6 changes: 3 additions & 3 deletions site/SiteNavigationTopic.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { EntryMeta } from "@ourworldindata/utils"
import { TagGraphNode } from "@ourworldindata/utils"

export const SiteNavigationTopic = ({ topic }: { topic: EntryMeta }) => {
export const SiteNavigationTopic = ({ topic }: { topic: TagGraphNode }) => {
return (
<li className="SiteNavigationTopic">
<a href={`/${topic.slug}`} data-track-note="header_navigation">
<span className="label">{topic.title}</span>
<span className="label">{topic.name}</span>
</a>
</li>
)
Expand Down
Loading
Loading