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 {
|
||||
BugIcon,
|
||||
LightBulbIcon,
|
||||
CodeIcon,
|
||||
GearIcon,
|
||||
RocketIcon,
|
||||
BeakerIcon,
|
||||
CopilotIcon,
|
||||
HubotIcon,
|
||||
LogIcon,
|
||||
TerminalIcon,
|
||||
BookIcon,
|
||||
ShieldLockIcon,
|
||||
LockIcon,
|
||||
} from '@primer/octicons-react'
|
||||
import { ValidOcticon, getOcticonComponent } from '../lib/octicons'
|
||||
|
||||
const Icons = {
|
||||
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 IconType = ValidOcticon
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
@@ -69,11 +39,7 @@ export const CookBookArticleCard = ({
|
||||
url,
|
||||
spotlight = false,
|
||||
}: Props) => {
|
||||
const setIcon = (icon: keyof typeof Icons) => {
|
||||
return Icons[icon] || CopilotIcon
|
||||
}
|
||||
|
||||
const IconComponent = setIcon(icon as keyof typeof Icons)
|
||||
const IconComponent = getOcticonComponent(icon)
|
||||
return (
|
||||
<div className="m-2">
|
||||
<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
|
||||
export type BaseTocItem = {
|
||||
fullPath: string
|
||||
@@ -5,22 +11,6 @@ export type BaseTocItem = {
|
||||
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
|
||||
export type ChildTocItem = BaseTocItem & {
|
||||
octicon?: ValidOcticon | null
|
||||
@@ -54,26 +44,6 @@ export type 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
|
||||
export type SimpleTocItem = {
|
||||
fullPath: string
|
||||
|
||||
Reference in New Issue
Block a user