Skip to content

Commit 03c1c34

Browse files
heiskrCopilot
andauthored
Tighten code comments in src/versions (#63462)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c52b57f-2c29-4aa1-8233-99d0d6e13571
1 parent 8b096ee commit 03c1c34

15 files changed

Lines changed: 126 additions & 229 deletions

‎src/versions/components/DeprecationBanner.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ export const DeprecationBanner = () => {
1616
return null
1717
}
1818

19-
// Have to "trick" TypeScript here because by default, this is an
20-
// optional key. But because we're confident with the JS business
21-
// logic in MainContext.tsx, we can safely assume that this key
22-
// is present.
19+
// MainContext supplies enterprise_deprecation before React renders this banner.
2320
const enterpriseDeprecation = data.reusables.enterprise_deprecation as EnterpriseDeprecation
2421
const message = enterpriseServerReleases.isOldestReleaseDeprecated
2522
? enterpriseDeprecation.version_was_deprecated

‎src/versions/components/VersionPicker.module.scss‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
/*
2-
* The header variant's styling is shared with the language picker and lives in
3-
* @/frame/components/page-header/HeaderPicker.module.scss. Only the default
4-
* (non-header) variant is styled here.
5-
*/
1+
// The header variant shares HeaderPicker.module.scss with the language picker.
2+
// This file styles the default variant.
63

74
.itemsWidth {
85
width: 14rem;

‎src/versions/components/VersionPicker.tsx‎

Lines changed: 27 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion'
1414
import { useTranslation } from '@/languages/components/useTranslation'
1515

1616
import styles from './VersionPicker.module.scss'
17-
// The header variant's trigger, menu surface and rows are shared with the language
18-
// picker so the two dropdowns cannot drift apart.
17+
// The header variant shares HeaderPicker.module.scss with the language picker,
18+
// so the two dropdowns stay in sync.
1919
import headerStyles from '@/frame/components/page-header/HeaderPicker.module.scss'
2020

2121
type Props = {
@@ -27,8 +27,8 @@ type VersionPickerLink = {
2727
text: string
2828
selected: boolean
2929
href: string
30-
// Brand's ActionMenu identifies the chosen row by string value, so every row needs
31-
// one. Versions use their own version name; the two extra rows use sentinels.
30+
// Brand's ActionMenu identifies rows by string value. Versions use their version
31+
// name; extra rows use sentinels.
3232
value: string
3333
extra: {
3434
arrow: boolean
@@ -41,82 +41,69 @@ type VersionPickerLink = {
4141
const ALL_RELEASES_VALUE = 'all-enterprise-releases'
4242
const ABOUT_VERSIONS_VALUE = 'about-versions'
4343

44-
// Brand clones ActionMenu.Button with its own ref, so the trigger cannot be reached
45-
// through a React ref. A stable test id keeps both the Escape handler and the tests
46-
// off Brand's hashed CSS class names.
44+
// Brand clones ActionMenu.Button with its own ref, so React refs cannot reach the
45+
// trigger. A stable test id keeps the Escape handler and tests off hashed CSS classes.
4746
const HEADER_TRIGGER_TESTID = 'version-picker-button'
4847

4948
type PlanMenuItemProps = {
5049
item: VersionPickerLink
51-
// Injected by ActionMenu.Overlay, which clones each of its direct children with the
52-
// select handler and the selection type derived from `selectionVariant`.
50+
// ActionMenu.Overlay injects handler and type into each direct child.
5351
handler?: (value: string) => void
5452
type?: 'none' | 'single' | 'link'
5553
}
5654

55+
// Extra rows opt out of Brand selection semantics because axe rejects aria-checked
56+
// on menuitem, and Brand derives both role and aria-checked from type.
5757
const PlanMenuItem = ({ item, handler, type }: PlanMenuItemProps) => {
5858
const isExtra = Boolean(item.extra.arrow || item.extra.info)
5959

6060
return (
6161
<BrandActionMenu.Item
6262
handler={handler}
63-
// Brand derives both `role` and `aria-checked` from `type`. The two extra rows
64-
// navigate elsewhere instead of choosing a version, so under the injected
65-
// 'single' they would render role="menuitem" *plus* aria-checked — which axe
66-
// rejects, since aria-checked is not an allowed attribute on menuitem. Overlay
67-
// injects `type` into its direct children only, so this wrapper is the seam
68-
// where a single row can opt out of selection semantics.
6963
type={isExtra ? 'none' : type}
7064
value={item.value}
7165
selected={item.selected}
7266
className={cx(
7367
headerStyles.headerMenuItem,
7468
item.selected && headerStyles.headerMenuItemSelected,
7569
)}
76-
// Only spread `role` for the extras: passing `role={undefined}` would override
77-
// the role Brand computes and leave the version rows with no role at all.
70+
// Only spread role for extras; role undefined overrides Brand's computed role.
7871
{...(isExtra ? { role: 'menuitem' } : {})}
7972
>
8073
<span data-testid="version-picker-item" className={headerStyles.headerMenuItemLabel}>
8174
{item.text}
8275
{item.extra.arrow && <ArrowRightIcon verticalAlign="middle" size={15} className="ml-1" />}
8376
{item.extra.info && <InfoIcon verticalAlign="middle" size={15} className="ml-1" />}
8477
</span>
85-
{/* The design marks the current plan with a trailing green dot instead of
86-
Brand's leading check icon, which the stylesheet hides. */}
78+
{/* HeaderPicker.module.scss hides Brand's leading check icon; design uses a trailing green dot. */}
8779
{item.selected && <DotFillIcon size={16} className={headerStyles.headerMenuItemDot} />}
8880
</BrandActionMenu.Item>
8981
)
9082
}
9183

92-
// The rule between the version rows and the two navigation rows. Brand has no divider
93-
// child, and ActionMenu.Overlay clones every direct child with `handler` and `type`,
94-
// so this wrapper takes no props at all: the injected ones are swallowed here instead
95-
// of landing on the DOM node. The <li> carries no tabIndex and no `data-value`, so
96-
// Brand's focus zone and its Enter handler both skip it — and it is never the menu's
97-
// first or last <li>, which are the two rows Brand wires its arrow-key wrap-around to.
84+
// Brand lacks a divider child, and ActionMenu.Overlay injects handler and type into
85+
// every direct child. This wrapper swallows those props so they do not reach the li.
86+
// Without tabIndex or data-value, Brand's focus zone and Enter handler skip the
87+
// separator. The caller keeps it away from the first and last li, which Brand uses
88+
// for arrow-key wrap-around.
9889
const PlanMenuSeparator = () => <li role="separator" className={headerStyles.headerMenuSeparator} />
9990

91+
// VersionPicker uses startsWith to identify Enterprise Server because VersionItem
92+
// omits hasNumberedReleases. The label says "version" for Enterprise Server because
93+
// versionTitle includes the numbered release; a "plan" label would make screen
94+
// readers announce "Select your plan: Enterprise Server 3.19".
10095
export const VersionPicker = ({ variant = 'default', onNavigate }: Props) => {
10196
const router = useRouter()
10297
const { currentVersion } = useVersion()
10398
const mainContext = useMainContext()
10499
const [open, setOpen] = useState(false)
105100
const pickerId = useId()
106101
const isHeader = variant === 'header'
107-
// Use TypeScript's "not null assertion" because mainContext.page should
108-
// be present in mainContext if it's gotten to the stage of React
109-
// rendering.
102+
// React rendering only starts after MainContext adds page.
110103
const page = mainContext.page!
111104
const { allVersions, enterpriseServerVersions } = mainContext
112105
const { t } = useTranslation(['pages', 'picker'])
113106

114-
// The same control chooses a plan on dotcom and Enterprise Cloud but a numbered
115-
// release on Enterprise Server, where `versionTitle` is `${planTitle} ${release}`.
116-
// A single "Select your plan:" would announce "Select your plan: Enterprise
117-
// Server 3.19" to screen readers. Uses the same `startsWith` predicate as
118-
// `hasEnterpriseVersions` below: `hasNumberedReleases` is set on the runtime
119-
// version object but is not declared on the `VersionItem` type.
120107
const pickerLabel = currentVersion.startsWith('enterprise-server')
121108
? t('version_picker_label')
122109
: t('plan_picker_label')
@@ -195,7 +182,7 @@ export const VersionPicker = ({ variant = 'default', onNavigate }: Props) => {
195182
const selectedOption = allLinks.find((item) => item.selected)
196183

197184
const handleVersionSelect = (item: VersionPickerLink) => {
198-
// Save the user's version preference when they actively select one
185+
// Navigation rows leave the existing version preference alone.
199186
if (item.extra?.version) {
200187
try {
201188
Cookies.set(USER_VERSION_COOKIE_NAME, item.extra.version)
@@ -204,26 +191,20 @@ export const VersionPicker = ({ variant = 'default', onNavigate }: Props) => {
204191
}
205192
}
206193
setOpen(false)
207-
// Navigate after setting cookie
194+
// Set the cookie before navigation so the next page can read the preference.
208195
if (item.href) {
209196
onNavigate?.()
210197
router.push(item.href)
211198
}
212199
}
213200

214201
if (isHeader) {
215-
// The Figma dropdown node draws no divider, but the rule that separated the
216-
// versions from the two navigation rows is kept from the @primer/react menu this
217-
// replaced. The filter keeps it from ever becoming the menu's first or last row:
218-
// Brand focuses the first <li> and binds its arrow-key wrap-around to the first
219-
// and the last, and neither should land on a separator.
202+
// Keep the separator from the default picker, but not where Brand focuses or wraps rows.
220203
const headerLinks = allLinks.filter(
221204
(item, index) => !item.divider || (index > 0 && index < allLinks.length - 1),
222205
)
223206

224-
// Brand reports the chosen row by value. Routing every row — the two extras
225-
// included — back through handleVersionSelect keeps navigation client-side
226-
// instead of letting the extras become anchors that reload the page.
207+
// Route extra rows through handleVersionSelect so they stay client-side.
227208
const handleHeaderSelect = (value: string) => {
228209
const item = headerLinks.find((link) => link.value === value)
229210
if (item) {
@@ -239,15 +220,10 @@ export const VersionPicker = ({ variant = 'default', onNavigate }: Props) => {
239220
)
240221
if (trigger?.getAttribute('aria-expanded') !== 'true') return
241222

242-
// Brand's ActionMenu and SubdomainNavBar both listen for Escape on `document`
243-
// and neither honours defaultPrevented, so a single Escape would close this
244-
// picker *and* the surrounding narrow menu. Stopping the event here — while it
245-
// is still in its capture phase, before it reaches either listener — leaves the
246-
// outer menu open. Brand has no controlled `open` prop, so the picker is closed
247-
// through its own trigger: focus it first so focus stays put, then click it to
248-
// let ActionMenu toggle itself shut.
223+
// Stop Escape in capture so SubdomainNavBar's document listener leaves the narrow menu open.
249224
event.preventDefault()
250225
event.stopPropagation()
226+
// Brand has no controlled open prop, so click its focused trigger to close it.
251227
trigger.focus()
252228
trigger.click()
253229
}

‎src/versions/lib/all-versions.ts‎

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import fs from 'fs'
22
import type { AllVersions, Version } from '@/types'
33
import enterpriseServerReleases from './enterprise-server-releases'
44

5-
// version = "plan"@"release"
6-
// example: enterprise-server@2.21
7-
// where "enterprise-server" is the plan and "2.21" is the release
5+
// Version keys combine plan and release, for example enterprise-server@2.21.
86
const versionDelimiter = '@'
97
const latestNonNumberedRelease = 'latest'
108
const REST_DATA_META_FILE = 'src/rest/lib/config.json'
@@ -27,26 +25,23 @@ interface RestApiConfig {
2725
}
2826
}
2927

30-
// !Explanation of versionless redirect fallbacks!
31-
// This array is **in order** of the versions the site should try to fall back to if
32-
// no version is provided in a URL. For example, if /foo refers to a page that is available
33-
// in all versions, we should not redirect it (because /foo is the correct FPT versioned URL).
34-
// But if /foo refers to a page that is only available in GHEC and GHES, we should redirect it
35-
// to /enterprise-cloud@latest/foo (since GHEC comes first in the hierarchy of version fallbacks).
36-
// The implementation lives in lib/redirects/permalinks.ts.
28+
// Versionless redirects try these plans in order. If /foo supports every plan, it
29+
// stays the Free, Pro, and Team URL. If it supports only Enterprise Cloud and
30+
// Enterprise Server, src/redirects/lib/permalinks.ts redirects it to
31+
// /enterprise-cloud@latest/foo.
3732
const plans: PlanConfig[] = [
3833
{
39-
// free-pro-team is **not** a user-facing version and is stripped from URLs.
40-
// See lib/remove-fpt-from-path.ts for details.
34+
// free-pro-team is not user-facing.
35+
// src/versions/lib/remove-fpt-from-path.ts strips it from URLs.
4136
plan: 'free-pro-team',
4237
planTitle: 'Free, Pro, & Team',
4338
shortName: 'fpt',
4439
releases: [latestNonNumberedRelease],
4540
latestRelease: latestNonNumberedRelease,
46-
nonEnterpriseDefault: true, // permanent way to refer to this plan if the name changes
41+
nonEnterpriseDefault: true, // Marks the non-enterprise default independently of the plan name.
4742
hasNumberedReleases: false,
48-
openApiBaseName: 'fpt', // used for REST
49-
miscBaseName: 'dotcom', // used for GraphQL and webhooks
43+
openApiBaseName: 'fpt', // REST base name.
44+
miscBaseName: 'dotcom', // Search index version map base name.
5045
},
5146
{
5247
plan: 'enterprise-cloud',
@@ -72,8 +67,6 @@ const plans: PlanConfig[] = [
7267

7368
const allVersions: AllVersions = {}
7469

75-
// combine the plans and releases to get allVersions object
76-
// e.g. free-pro-team@latest, enterprise-server@2.21, enterprise-server@2.20, etc.
7770
for (const planObj of plans) {
7871
for (const release of planObj.releases) {
7972
const version = `${planObj.plan}${versionDelimiter}${release}`
@@ -91,8 +84,10 @@ for (const planObj of plans) {
9184
miscVersionName: planObj.hasNumberedReleases
9285
? `${planObj.miscBaseName}${release}`
9386
: planObj.miscBaseName,
94-
apiVersions: [], // REST Calendar Date Versions, this may be empty for non calendar date versioned products
95-
latestApiVersion: '', // Latest REST Calendar Date Version, this may be empty for non calendar date versioned products
87+
// REST calendar date versions; empty for products without calendar date API versions.
88+
apiVersions: [],
89+
// Latest REST calendar date version; empty for products without calendar date API versions.
90+
latestApiVersion: '',
9691
plan: planObj.plan,
9792
planTitle: planObj.planTitle,
9893
shortName: planObj.shortName,
@@ -108,15 +103,15 @@ for (const planObj of plans) {
108103
}
109104
}
110105

111-
// Adds the calendar date (or api versions) to the allVersions object
106+
// REST config adds calendar date API versions after the version objects exist.
112107
const apiVersions: RestApiConfig['api-versions'] = JSON.parse(
113108
fs.readFileSync(REST_DATA_META_FILE, 'utf8'),
114109
)['api-versions']
115110

116111
for (const key of Object.keys(apiVersions)) {
117112
const docsVersion = getDocsVersion(key)
118113
allVersions[docsVersion].apiVersions.push(...apiVersions[key].sort().reverse())
119-
// Create a copy of the array to avoid mutating the original when using pop()
114+
// Copy before pop so latestApiVersion does not remove a version from apiVersions.
120115
const sortedVersions = [...apiVersions[key].sort()]
121116
allVersions[docsVersion].latestApiVersion = sortedVersions.pop() || ''
122117
}
@@ -130,9 +125,7 @@ export function isApiVersioned(version: string): boolean {
130125
return allVersions[version] && allVersions[version].apiVersions.length > 0
131126
}
132127

133-
// Currently the versions from the OpenAPI do not match the versions on Docs.
134-
// There is a mapping between the version names. This gets the Docs version from
135-
// the OpenAPI version name.
128+
// OpenAPI names do not match Docs version names, so this maps one to its Docs version.
136129
export function getDocsVersion(openApiVersion: string): string {
137130
const matchingVersion = Object.values(allVersions).find((version) =>
138131
openApiVersion.startsWith(version.openApiVersionName),

‎src/versions/lib/enterprise-server-releases.d.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
type Dates = {
22
[key: string]: {
3-
releaseDate: string // For backward compatibility - will be RC date initially, then GA date once available
3+
// Templates read releaseDate as the display date: RC date until the GA date exists.
4+
releaseDate: string
45
deprecationDate: string
5-
releaseCandidateDate?: string // Release Candidate date
6-
generalAvailabilityDate?: string // General Availability date
7-
displayCandidateDate?: string | null // Computed: RC date if in past, null if future
8-
displayReleaseDate?: string | null // Computed: GA date if in past, null if future
6+
releaseCandidateDate?: string
7+
generalAvailabilityDate?: string
8+
// Templates hide release dates until each date has passed.
9+
displayCandidateDate?: string | null
10+
displayReleaseDate?: string | null
911
}
1012
}
1113

‎src/versions/lib/enterprise-server-releases.ts‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,18 @@ const rawDates: RawDatesData = JSON.parse(
2424
fs.readFileSync('src/ghes-releases/lib/enterprise-dates.json', 'utf8'),
2525
)
2626

27-
// Upcoming GHES release numbers (used in frontmatter and release planning)
27+
// Frontmatter and release planning use the next two GHES release numbers.
2828
export const next = '3.23'
2929
export const nextNext = '3.24'
3030

31-
// Currently supported GHES versions (in descending order, latest first)
31+
// Keep supported GHES versions in descending order, latest first.
3232
export const supported = ['3.22', '3.21', '3.20', '3.19', '3.18', '3.17']
3333

34-
// Set to version number when in RC phase, null when no RC is active
34+
// Use the release number during an active RC; use null outside RC.
3535
export const releaseCandidate = null
3636

37-
// Deprecated versions with functional redirect handling (3.0+)
38-
// When archiving a new version, add it here and update the archival process
37+
// Deprecated releases from 3.0 onward use functional redirects.
38+
// Add a newly archived release here and update the archival process.
3939
export const deprecatedWithFunctionalRedirects = [
4040
'3.16',
4141
'3.15',
@@ -56,7 +56,7 @@ export const deprecatedWithFunctionalRedirects = [
5656
'3.0',
5757
]
5858

59-
// All deprecated versions (combines functional + legacy redirect handling)
59+
// The deprecated list combines functional redirects with legacy redirect handling.
6060
export const deprecated = [
6161
...deprecatedWithFunctionalRedirects,
6262
'2.22',
@@ -85,13 +85,13 @@ export const deprecated = [
8585
'11.10.340',
8686
]
8787

88-
// Versions with legacy asset handling (stored in separate repos before blob storage)
88+
// Legacy asset releases store assets in separate repos instead of blob storage.
8989
export const legacyAssetVersions = ['3.0', '2.22', '2.21']
9090

9191
export const firstReleaseStoredInBlobStorage = '3.2'
9292
export const firstVersionDeprecatedOnNewSite = '2.13'
9393
export const lastVersionWithoutArchivedRedirectsFile = '2.17'
94-
export const lastReleaseWithLegacyFormat = '2.18' // Last to use /enterprise/<release>/... paths
94+
export const lastReleaseWithLegacyFormat = '2.18' // Last release with /enterprise/<release>/... paths.
9595
export const firstReleaseNote = '2.20'
9696
export const firstRestoredAdminGuides = '2.21'
9797

@@ -101,7 +101,7 @@ export const latest = supported[0]
101101
export const latestStable = releaseCandidate ? supported[1] : latest
102102
export const oldestSupported = supported[supported.length - 1]
103103

104-
// Enhanced dates object with computed display values for templates
104+
// Templates read these computed display dates to hide future release dates.
105105
export const dates: Record<string, EnhancedVersionDateData> = Object.fromEntries(
106106
Object.entries(rawDates).map(([version, versionData]) => [
107107
version,
@@ -118,8 +118,7 @@ export const isOldestReleaseDeprecated = nextDeprecationDate
118118
? new Date() > new Date(nextDeprecationDate)
119119
: false
120120

121-
// Find any other releases that may share the oldest deprecation date
122-
// We'll want to display the deprecation banner on all of these releases (not just oldest)
121+
// Show the deprecation banner on every release that shares the oldest deprecation date.
123122
export const releasesWithOldestDeprecationDate = Object.entries(dates)
124123
.filter(([, versionData]) => versionData.deprecationDate === nextDeprecationDate)
125124
.map(([version]) => version)
@@ -140,8 +139,8 @@ export const deprecatedReleasesOnDeveloperSite = deprecated.filter((version) =>
140139
versionSatisfiesRange(version, '<=2.16'),
141140
)
142141

143-
// Returns the date only once it has passed, so we never advertise a future
144-
// release date. An unparseable date gives NaN, which also returns null.
142+
// Return a date only after it has passed, so templates never advertise future releases.
143+
// Unparseable dates produce NaN, which also returns null.
145144
function processDateForDisplay(date: string | undefined): string | null {
146145
if (!date) return null
147146
const currentTimestamp = Math.floor(Date.now() / 1000)

0 commit comments

Comments
 (0)