Consolidate octicon frontmatter property references into single file (#56342)
Co-authored-by: Robert Sese <734194+rsese@users.noreply.github.com>
This commit is contained in:
@@ -1,37 +1,7 @@
|
|||||||
import { Label, LabelGroup, Link } from '@primer/react'
|
import { Label, LabelGroup, Link } from '@primer/react'
|
||||||
import {
|
import { ValidOcticon, getOcticonComponent } from '../lib/octicons'
|
||||||
BugIcon,
|
|
||||||
LightBulbIcon,
|
|
||||||
CodeIcon,
|
|
||||||
GearIcon,
|
|
||||||
RocketIcon,
|
|
||||||
BeakerIcon,
|
|
||||||
CopilotIcon,
|
|
||||||
HubotIcon,
|
|
||||||
LogIcon,
|
|
||||||
TerminalIcon,
|
|
||||||
BookIcon,
|
|
||||||
ShieldLockIcon,
|
|
||||||
LockIcon,
|
|
||||||
} from '@primer/octicons-react'
|
|
||||||
|
|
||||||
const Icons = {
|
type IconType = ValidOcticon
|
||||||
bug: BugIcon,
|
|
||||||
lightbulb: LightBulbIcon,
|
|
||||||
code: CodeIcon,
|
|
||||||
gear: GearIcon,
|
|
||||||
rocket: RocketIcon,
|
|
||||||
beaker: BeakerIcon,
|
|
||||||
copilot: CopilotIcon,
|
|
||||||
hubot: HubotIcon,
|
|
||||||
log: LogIcon,
|
|
||||||
terminal: TerminalIcon,
|
|
||||||
book: BookIcon,
|
|
||||||
'shield-lock': ShieldLockIcon,
|
|
||||||
lock: LockIcon,
|
|
||||||
}
|
|
||||||
|
|
||||||
type IconType = keyof typeof Icons
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
@@ -69,11 +39,7 @@ export const CookBookArticleCard = ({
|
|||||||
url,
|
url,
|
||||||
spotlight = false,
|
spotlight = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const setIcon = (icon: keyof typeof Icons) => {
|
const IconComponent = getOcticonComponent(icon)
|
||||||
return Icons[icon] || CopilotIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
const IconComponent = setIcon(icon as keyof typeof Icons)
|
|
||||||
return (
|
return (
|
||||||
<div className="m-2">
|
<div className="m-2">
|
||||||
<div
|
<div
|
||||||
|
|||||||
66
src/landings/lib/octicons.ts
Normal file
66
src/landings/lib/octicons.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
BugIcon,
|
||||||
|
LightBulbIcon,
|
||||||
|
CodeIcon,
|
||||||
|
GearIcon,
|
||||||
|
RocketIcon,
|
||||||
|
BeakerIcon,
|
||||||
|
CopilotIcon,
|
||||||
|
HubotIcon,
|
||||||
|
LogIcon,
|
||||||
|
TerminalIcon,
|
||||||
|
BookIcon,
|
||||||
|
ShieldLockIcon,
|
||||||
|
LockIcon,
|
||||||
|
} from '@primer/octicons-react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of octicon names to their React components
|
||||||
|
* This is the single source of truth for all supported octicons
|
||||||
|
*/
|
||||||
|
export const OCTICON_COMPONENTS = {
|
||||||
|
bug: BugIcon,
|
||||||
|
lightbulb: LightBulbIcon,
|
||||||
|
code: CodeIcon,
|
||||||
|
gear: GearIcon,
|
||||||
|
rocket: RocketIcon,
|
||||||
|
beaker: BeakerIcon,
|
||||||
|
copilot: CopilotIcon,
|
||||||
|
hubot: HubotIcon,
|
||||||
|
log: LogIcon,
|
||||||
|
terminal: TerminalIcon,
|
||||||
|
book: BookIcon,
|
||||||
|
'shield-lock': ShieldLockIcon,
|
||||||
|
lock: LockIcon,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid octicon types derived from the component mapping
|
||||||
|
*/
|
||||||
|
export type ValidOcticon = keyof typeof OCTICON_COMPONENTS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of all valid octicon names for validation, derived from component mapping
|
||||||
|
*/
|
||||||
|
export const VALID_OCTICONS = Object.keys(OCTICON_COMPONENTS) as ValidOcticon[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to validate and cast octicon values
|
||||||
|
* @param octicon - The octicon string to validate
|
||||||
|
* @returns True if the octicon is valid, false otherwise
|
||||||
|
*/
|
||||||
|
export function isValidOcticon(octicon: string | null): octicon is ValidOcticon {
|
||||||
|
return octicon !== null && (octicon as ValidOcticon) in OCTICON_COMPONENTS
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the React component for a given octicon name
|
||||||
|
* @param octicon - The octicon name
|
||||||
|
* @returns The corresponding React component, or CopilotIcon as fallback
|
||||||
|
*/
|
||||||
|
export function getOcticonComponent(octicon: ValidOcticon | undefined) {
|
||||||
|
if (!octicon || !isValidOcticon(octicon)) {
|
||||||
|
return CopilotIcon
|
||||||
|
}
|
||||||
|
return OCTICON_COMPONENTS[octicon] || CopilotIcon
|
||||||
|
}
|
||||||
148
src/landings/tests/octicons.test.ts
Normal file
148
src/landings/tests/octicons.test.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
import {
|
||||||
|
ValidOcticon,
|
||||||
|
VALID_OCTICONS,
|
||||||
|
OCTICON_COMPONENTS,
|
||||||
|
isValidOcticon,
|
||||||
|
getOcticonComponent,
|
||||||
|
} from '../lib/octicons'
|
||||||
|
import { CopilotIcon, BugIcon, RocketIcon } from '@primer/octicons-react'
|
||||||
|
|
||||||
|
describe('octicons reference', () => {
|
||||||
|
describe('VALID_OCTICONS', () => {
|
||||||
|
test('contains expected octicon names', () => {
|
||||||
|
// Test that we have the expected number of octicons and they're all defined
|
||||||
|
expect(VALID_OCTICONS.length).toBeGreaterThan(0)
|
||||||
|
expect(VALID_OCTICONS).toEqual(expect.arrayContaining(['bug', 'rocket', 'copilot']))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('all octicons are strings', () => {
|
||||||
|
VALID_OCTICONS.forEach((octicon) => {
|
||||||
|
expect(typeof octicon).toBe('string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('OCTICON_COMPONENTS', () => {
|
||||||
|
test('has components for all valid octicons', () => {
|
||||||
|
VALID_OCTICONS.forEach((octicon) => {
|
||||||
|
expect(OCTICON_COMPONENTS[octicon]).toBeDefined()
|
||||||
|
expect(typeof OCTICON_COMPONENTS[octicon]).toBe('object')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('maps specific octicons to correct components', () => {
|
||||||
|
expect(OCTICON_COMPONENTS.bug).toBe(BugIcon)
|
||||||
|
expect(OCTICON_COMPONENTS.rocket).toBe(RocketIcon)
|
||||||
|
expect(OCTICON_COMPONENTS.copilot).toBe(CopilotIcon)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isValidOcticon', () => {
|
||||||
|
test('returns true for valid octicons', () => {
|
||||||
|
expect(isValidOcticon('bug')).toBe(true)
|
||||||
|
expect(isValidOcticon('rocket')).toBe(true)
|
||||||
|
expect(isValidOcticon('shield-lock')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false for invalid octicons', () => {
|
||||||
|
expect(isValidOcticon('invalid-octicon')).toBe(false)
|
||||||
|
expect(isValidOcticon('pizza')).toBe(false)
|
||||||
|
expect(isValidOcticon('')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false for null or undefined', () => {
|
||||||
|
expect(isValidOcticon(null)).toBe(false)
|
||||||
|
expect(isValidOcticon(undefined as any)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('provides correct type narrowing', () => {
|
||||||
|
const testOcticon: string | null = 'bug'
|
||||||
|
|
||||||
|
if (isValidOcticon(testOcticon)) {
|
||||||
|
// This should compile without type errors
|
||||||
|
const validOcticon: ValidOcticon = testOcticon
|
||||||
|
expect(validOcticon).toBe('bug')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getOcticonComponent', () => {
|
||||||
|
test('returns correct component for valid octicons', () => {
|
||||||
|
expect(getOcticonComponent('bug')).toBe(BugIcon)
|
||||||
|
expect(getOcticonComponent('rocket')).toBe(RocketIcon)
|
||||||
|
expect(getOcticonComponent('copilot')).toBe(CopilotIcon)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns CopilotIcon as fallback for undefined', () => {
|
||||||
|
expect(getOcticonComponent(undefined)).toBe(CopilotIcon)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns CopilotIcon as fallback for invalid octicons', () => {
|
||||||
|
// TypeScript should prevent this, but test runtime behavior
|
||||||
|
expect(getOcticonComponent('invalid' as ValidOcticon)).toBe(CopilotIcon)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('type safety', () => {
|
||||||
|
test('ValidOcticon type includes all expected values', () => {
|
||||||
|
// This test ensures the type system prevents invalid octicons at compile time
|
||||||
|
// Test a few key octicons to verify the type works correctly
|
||||||
|
const testOcticons: ValidOcticon[] = ['bug', 'rocket', 'copilot']
|
||||||
|
|
||||||
|
testOcticons.forEach((octicon) => {
|
||||||
|
expect(VALID_OCTICONS.includes(octicon)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('consistency checks', () => {
|
||||||
|
test('OCTICON_COMPONENTS keys match VALID_OCTICONS', () => {
|
||||||
|
const componentKeys = Object.keys(OCTICON_COMPONENTS)
|
||||||
|
const validOcticonsSet = new Set(VALID_OCTICONS)
|
||||||
|
|
||||||
|
componentKeys.forEach((key) => {
|
||||||
|
expect(validOcticonsSet.has(key as ValidOcticon)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(componentKeys).toHaveLength(VALID_OCTICONS.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('no duplicate octicons in VALID_OCTICONS', () => {
|
||||||
|
const octiconsSet = new Set(VALID_OCTICONS)
|
||||||
|
expect(octiconsSet.size).toBe(VALID_OCTICONS.length)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('single source of truth', () => {
|
||||||
|
test('VALID_OCTICONS is derived from OCTICON_COMPONENTS', () => {
|
||||||
|
const componentKeys = Object.keys(OCTICON_COMPONENTS).sort()
|
||||||
|
const validOcticons = [...VALID_OCTICONS].sort()
|
||||||
|
|
||||||
|
expect(validOcticons).toEqual(componentKeys)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ValidOcticon type matches OCTICON_COMPONENTS keys', () => {
|
||||||
|
// This test ensures the type system is correctly derived from the object
|
||||||
|
const testOcticon: ValidOcticon = 'bug'
|
||||||
|
expect(OCTICON_COMPONENTS[testOcticon]).toBeDefined()
|
||||||
|
|
||||||
|
// Type check - this should compile without errors
|
||||||
|
const allKeys: ValidOcticon[] = Object.keys(OCTICON_COMPONENTS) as ValidOcticon[]
|
||||||
|
expect(allKeys.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('adding new octicon only requires updating OCTICON_COMPONENTS', () => {
|
||||||
|
// This test documents the single source of truth approach
|
||||||
|
// If you add a new octicon to OCTICON_COMPONENTS:
|
||||||
|
// 1. ValidOcticon type automatically includes it
|
||||||
|
// 2. VALID_OCTICONS array automatically includes it
|
||||||
|
// 3. All validation functions work with it
|
||||||
|
|
||||||
|
const componentCount = Object.keys(OCTICON_COMPONENTS).length
|
||||||
|
const validOcticonsCount = VALID_OCTICONS.length
|
||||||
|
|
||||||
|
expect(componentCount).toBe(validOcticonsCount)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { ValidOcticon, isValidOcticon } from './lib/octicons'
|
||||||
|
|
||||||
|
// Re-export ValidOcticon and isValidOcticon for compatibility with existing imports
|
||||||
|
export type { ValidOcticon }
|
||||||
|
export { isValidOcticon }
|
||||||
|
|
||||||
// Base type for all TOC items with core properties
|
// Base type for all TOC items with core properties
|
||||||
export type BaseTocItem = {
|
export type BaseTocItem = {
|
||||||
fullPath: string
|
fullPath: string
|
||||||
@@ -5,22 +11,6 @@ export type BaseTocItem = {
|
|||||||
intro?: string | null
|
intro?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid octicon types that match the CookBookArticleCard component
|
|
||||||
export type ValidOcticon =
|
|
||||||
| 'code'
|
|
||||||
| 'log'
|
|
||||||
| 'terminal'
|
|
||||||
| 'bug'
|
|
||||||
| 'lightbulb'
|
|
||||||
| 'gear'
|
|
||||||
| 'rocket'
|
|
||||||
| 'beaker'
|
|
||||||
| 'copilot'
|
|
||||||
| 'hubot'
|
|
||||||
| 'book'
|
|
||||||
| 'shield-lock'
|
|
||||||
| 'lock'
|
|
||||||
|
|
||||||
// Extended type for child TOC items with additional metadata
|
// Extended type for child TOC items with additional metadata
|
||||||
export type ChildTocItem = BaseTocItem & {
|
export type ChildTocItem = BaseTocItem & {
|
||||||
octicon?: ValidOcticon | null
|
octicon?: ValidOcticon | null
|
||||||
@@ -54,26 +44,6 @@ export type RawTocItem = {
|
|||||||
childTocItems: RawTocItem[]
|
childTocItems: RawTocItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to validate and cast octicon values
|
|
||||||
export function isValidOcticon(octicon: string | null): octicon is ValidOcticon {
|
|
||||||
const validOcticons: ValidOcticon[] = [
|
|
||||||
'code',
|
|
||||||
'log',
|
|
||||||
'terminal',
|
|
||||||
'bug',
|
|
||||||
'lightbulb',
|
|
||||||
'gear',
|
|
||||||
'rocket',
|
|
||||||
'beaker',
|
|
||||||
'copilot',
|
|
||||||
'hubot',
|
|
||||||
'book',
|
|
||||||
'shield-lock',
|
|
||||||
'lock',
|
|
||||||
]
|
|
||||||
return octicon !== null && validOcticons.includes(octicon as ValidOcticon)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified TOC item type for basic landing pages that don't need extended metadata
|
// Simplified TOC item type for basic landing pages that don't need extended metadata
|
||||||
export type SimpleTocItem = {
|
export type SimpleTocItem = {
|
||||||
fullPath: string
|
fullPath: string
|
||||||
|
|||||||
Reference in New Issue
Block a user