Skip to content

Commit

Permalink
Merge pull request #3620 from serlo/use-new-path-field
Browse files Browse the repository at this point in the history
refactor: use new path field, simplify breadcrumbs code
  • Loading branch information
elbotho authored Apr 15, 2024
2 parents c9648b3 + 102b35e commit a113ec9
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 191 deletions.
20 changes: 20 additions & 0 deletions apps/web/graphql.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19347,6 +19347,26 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "path",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "TaxonomyTerm",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "taxonomyId",
"description": null,
Expand Down
240 changes: 102 additions & 138 deletions apps/web/src/fetcher/create-breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,98 +7,11 @@ import {
import { BreadcrumbsData, UuidType } from '@/data-types'
import { getInstanceDataByLang } from '@/helper/feature-i18n'

interface RecursiveTree {
title: string
alias: string
id: number
parent: RecursiveTree
}

type TaxonomyTermNodes = Extract<
MainUuidType,
{ __typename: 'Article' }
>['taxonomyTerms']['nodes']

function countParents(taxonomyPath: TaxonomyTermNodes[0]) {
function count(current: RecursiveTree): number {
if (current.parent) {
return count(current.parent) + 1
} else {
return 0
}
}
return count(taxonomyPath as RecursiveTree)
}

export function taxonomyParentsToRootToBreadcrumbsData(
taxonomyPath: TaxonomyTermNodes[0] | undefined,
instance: Instance
): BreadcrumbsData | undefined {
if (taxonomyPath === undefined) return undefined
// because we don't have infinite recursion, this is the best we can do now
// move this function into the API
let current = taxonomyPath as RecursiveTree
const breadcrumbs: BreadcrumbsData = []

const { secondaryMenus } = getInstanceDataByLang(instance)

function getSubjectName(current: RecursiveTree) {
// we what to short circuit taxonomy if entry is part of the meta menu
// In this case we need to walk to the subject and extract the name
let name = ''
while (current.parent && current.id !== 3) {
name = current.title
current = current.parent
}
return name
}

while (current.id !== 3 && current.parent) {
// find subject of this path and rewrite root breadcrumb
const matching = secondaryMenus.filter((menu) => menu.rootId === current.id)
if (matching.length > 0) {
breadcrumbs.unshift({
label: matching[0].rootName ?? current.title,
url: matching[0].landingUrl,
id: current.id,
})
break // not going further, especially for subjects under construction
} else {
// check if this breadcrumb is already part of secondary menu
const matching2 = secondaryMenus.filter((menu) =>
menu.entries.some((entry) => entry.id === current.id)
)
if (matching2.length > 0) {
const metaMenuEntry = matching2[0].entries.filter(
(menu) => menu.id === current.id
)[0]

breadcrumbs.unshift({
label: metaMenuEntry.title,
url: current.alias,
id: current.id,
})
breadcrumbs.unshift({
label: matching2[0].rootName ?? getSubjectName(current),
url: matching2[0].landingUrl,
id: matching2[0].rootId,
})
break
} else {
breadcrumbs.unshift({
label: current.title,
url: current.alias,
id: current.id,
})
}
}
// the recursion is limited, so there is a slight type mismatch
current = current.parent
}

return breadcrumbs
}

export function createBreadcrumbs(uuid: MainUuidType, instance: Instance) {
if (uuid.__typename === UuidType.TaxonomyTerm) {
return compat(taxonomyParentsToRootToBreadcrumbsData(uuid, instance))
Expand All @@ -119,68 +32,119 @@ export function createBreadcrumbs(uuid: MainUuidType, instance: Instance) {
return compat(buildFromTaxTerms(uuid.taxonomyTerms.nodes, instance))
}

function buildFromTaxTerms(
taxonomyPaths: TaxonomyTermNodes | undefined,
instance: Instance
) {
if (taxonomyPaths === undefined) return undefined

// we select first shortest taxonomy path as main taxonomy and show it in breadcrumbs
// this happens directly on taxonomy parents and is not taking short-cuircuits into account
let mainTax
let count = -1

for (const path of taxonomyPaths) {
if (!path) continue
if (count === -1 || count > countParents(path)) {
mainTax = path
count = countParents(mainTax)
}
return undefined
}

export function taxonomyParentsToRootToBreadcrumbsData(
term: TaxonomyTermNodes[0] | undefined,
instance: Instance,
includeFirstParent?: boolean
): BreadcrumbsData | undefined {
if (term === undefined) return undefined

const { secondaryMenus } = getInstanceDataByLang(instance)

let breadcrumbs = term.path.map((entry) => {
return {
label: entry!.title,
url: entry!.alias,
id: entry!.id,
}
})

if (!breadcrumbs?.length) return undefined

return taxonomyParentsToRootToBreadcrumbsData(mainTax, instance)
if (includeFirstParent) {
breadcrumbs.push({
label: term.title,
url: term.alias,
id: term.id,
})
}

function compat(breadcrumbs: BreadcrumbsData | undefined) {
if (!breadcrumbs) return undefined
// get the subject from the secondary menu data so we link to the correct landing pages
const subject = secondaryMenus.find(
(menu) => menu.rootId === breadcrumbs[0].id
)

if (subject) {
breadcrumbs[0].label = subject.rootName ?? breadcrumbs[0].label
breadcrumbs[0].url = subject.landingUrl ?? breadcrumbs[0].url

const breadcrumbIdsNoRoot = breadcrumbs.map((item) => item.id).slice(1)

// identify final exam taxonomies or their children
const isOrInExamsFolder = !!breadcrumbs.find(
({ id }) => id && mathExamsTaxonomies.includes(id)
// if there are other entries that exists in breadcrumbs and secondaryMenu
// we use that to shorten the breadcrumbs
// example: https://de.serlo.org/mathe/16299/klasse-11
const secondaryMenuEntry = subject?.entries.find((entry) =>
breadcrumbIdsNoRoot.includes(entry.id ?? 0)
)

// compat: remove last entry because it is the entry itself
if (uuid.__typename === UuidType.TaxonomyTerm && breadcrumbs.length > 1) {
breadcrumbs = breadcrumbs.slice(0, -1)
if (secondaryMenuEntry) {
const matchingBreadcrumbIndex = breadcrumbs.findIndex(
(item) => item.id === secondaryMenuEntry.id
)
const matchingBreadcrumbItem = breadcrumbs[matchingBreadcrumbIndex]
breadcrumbs = [
breadcrumbs[0],
{
...matchingBreadcrumbItem,
label: secondaryMenuEntry.title,
},
...breadcrumbs.slice(matchingBreadcrumbIndex + 1),
]
}
}

// compat: remove empty entries
breadcrumbs = breadcrumbs.filter(({ url, label }) => url && label)
return breadcrumbs
}

// compat: exam breadcrumbs remove "Deutschland" Taxonomie and maybe school type
if (isOrInExamsFolder) {
const exclude =
breadcrumbs.length > 4 ? [...schoolTaxonomies, 16030] : [16030]
return breadcrumbs.filter(({ id }) => id && !exclude.includes(id))
}
function buildFromTaxTerms(
nodes: TaxonomyTermNodes | undefined,
instance: Instance
) {
if (!nodes) return undefined

const shortened: BreadcrumbsData = []
breadcrumbs.map((entry, i, arr) => {
const maxItems = 4
const overflow = arr.length > maxItems
const itemsToRemove = arr.length - maxItems
const ellipsesItem = overflow && i === 2

if (overflow && i > 2 && i < 1 + itemsToRemove) return
// special case
if (arr.length - itemsToRemove > 4 && i === 1) return
if (ellipsesItem) {
shortened.push({ label: '', ellipsis: true })
} else {
shortened.push(entry)
}
})
// we select first shortest taxonomy path as main taxonomy and show it in breadcrumbs
// this happens directly on taxonomy parents and is not taking short-cuircuits into account
nodes.sort((a, b) => a.path.length - b.path.length)
return taxonomyParentsToRootToBreadcrumbsData(nodes[0], instance, true)
}

function compat(breadcrumbs?: BreadcrumbsData) {
if (!breadcrumbs || !breadcrumbs.length) return undefined

return shortened
// identify final exam taxonomies or their children
const isOrInExamsFolder = !!breadcrumbs.find(
({ id }) => id && mathExamsTaxonomies.includes(id)
)

// compat: remove empty entries
breadcrumbs = breadcrumbs.filter(({ url, label }) => url && label)

// compat: exam breadcrumbs remove "Deutschland" Taxonomie and maybe school type
if (isOrInExamsFolder) {
const exclude =
breadcrumbs.length > 4 ? [...schoolTaxonomies, 16030] : [16030]
return breadcrumbs.filter(({ id }) => id && !exclude.includes(id))
}

const shortened: BreadcrumbsData = []
breadcrumbs.map((entry, i, arr) => {
const maxItems = 4
const overflow = arr.length > maxItems
const itemsToRemove = arr.length - maxItems
const ellipsesItem = overflow && i === 2

if (overflow && i > 2 && i < 1 + itemsToRemove) return
// special case
if (arr.length - itemsToRemove > 4 && i === 1) return
if (ellipsesItem) {
shortened.push({ label: '', ellipsis: true })
} else {
shortened.push(entry)
}
})

return shortened
}
Loading

0 comments on commit a113ec9

Please sign in to comment.