diff --git a/components/DefaultLayout.tsx b/components/DefaultLayout.tsx index a9d0527371..4cccdd592f 100644 --- a/components/DefaultLayout.tsx +++ b/components/DefaultLayout.tsx @@ -45,7 +45,7 @@ export const DefaultLayout = (props: Props) => { -
+
diff --git a/components/Header.tsx b/components/Header.tsx index c9b5f5898e..09f686914e 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -22,7 +22,8 @@ export const Header = () => { const showVersionPicker = relativePath === 'index.md' || currentLayoutName === 'product-landing' || - currentLayoutName === 'product-sublanding' + currentLayoutName === 'product-sublanding' || + currentLayoutName === 'release-notes' return (
diff --git a/components/HeaderNotifications.tsx b/components/HeaderNotifications.tsx index 76057ec427..0e36161146 100644 --- a/components/HeaderNotifications.tsx +++ b/components/HeaderNotifications.tsx @@ -87,6 +87,7 @@ export const HeaderNotifications = () => { const isLast = i === allNotifications.length - 1 return (
( + ref: MutableRefObject | RefObject, + rootMargin: string = '0px' +): boolean { + const [isIntersecting, setIntersecting] = useState(false) + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + setIntersecting(entry.isIntersecting) + }, + { + rootMargin, + } + ) + if (ref.current) { + observer.observe(ref.current) + } + return () => { + ref.current && observer.unobserve(ref.current) + } + }, []) + return isIntersecting +} diff --git a/components/release-notes/GHAEReleaseNotePatch.tsx b/components/release-notes/GHAEReleaseNotePatch.tsx new file mode 100644 index 0000000000..530c910f4a --- /dev/null +++ b/components/release-notes/GHAEReleaseNotePatch.tsx @@ -0,0 +1,59 @@ +import { useRef, useEffect } from 'react' + +import { useTranslation } from 'components/hooks/useTranslation' +import { useOnScreen } from 'components/hooks/useOnScreen' +import { PatchNotes } from './PatchNotes' +import { ReleaseNotePatch } from './types' + +type Props = { patch: ReleaseNotePatch; didEnterView: () => void } +export function GHAEReleaseNotePatch({ patch, didEnterView }: Props) { + const { t } = useTranslation('release_notes') + const containerRef = useRef(null) + const onScreen = useOnScreen(containerRef, '-40% 0px -50%') + useEffect(() => { + if (onScreen) { + didEnterView() + } + }, [onScreen]) + + const bannerText = patch.currentWeek + ? t('banner_text_current') + : `${t('banner_text_past')} ${patch.friendlyDate}.` + + return ( +
+
+
+

{patch.title}

+ + {patch.release_candidate && ( + + Release Candidate + + )} + + +
+

+ {patch.friendlyDate} - {bannerText} +

+
+ +
+
+ + +
+
+ ) +} diff --git a/components/release-notes/GHAEReleaseNotes.tsx b/components/release-notes/GHAEReleaseNotes.tsx new file mode 100644 index 0000000000..96970987cf --- /dev/null +++ b/components/release-notes/GHAEReleaseNotes.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react' +import cx from 'classnames' +import { ChevronDownIcon } from '@primer/octicons-react' +import { GHAEReleaseNotePatch } from './GHAEReleaseNotePatch' +import { GHAEReleaseNotesContextT } from './types' + +type GitHubAEProps = { + context: GHAEReleaseNotesContextT +} +export function GHAEReleaseNotes({ context }: GitHubAEProps) { + const { releaseNotes, releases, currentVersion } = context + const [focusedPatch, setFocusedPatch] = useState('') + + return ( +
+
+
+
+

{currentVersion.planTitle} release notes

+
+
+ +
+ {releaseNotes.map((patch) => { + return ( + setFocusedPatch(patch.version)} + /> + ) + })} +
+
+ + +
+ ) +} diff --git a/components/release-notes/GHESReleaseNotePatch.tsx b/components/release-notes/GHESReleaseNotePatch.tsx new file mode 100644 index 0000000000..d7d3c66730 --- /dev/null +++ b/components/release-notes/GHESReleaseNotePatch.tsx @@ -0,0 +1,110 @@ +import { useEffect, useRef } from 'react' +import dayjs from 'dayjs' + +import { useTranslation } from 'components/hooks/useTranslation' +import { PatchNotes } from './PatchNotes' +import { Link } from 'components/Link' +import { CurrentVersion, ReleaseNotePatch, GHESMessage } from './types' +import { useOnScreen } from 'components/hooks/useOnScreen' + +type Props = { + patch: ReleaseNotePatch + currentVersion: CurrentVersion + latestPatch: string + latestRelease: string + message: GHESMessage + didEnterView: () => void +} +export function GHESReleaseNotePatch({ + patch, + currentVersion, + latestPatch, + latestRelease, + message, + didEnterView, +}: Props) { + const { t } = useTranslation('header') + const containerRef = useRef(null) + const onScreen = useOnScreen(containerRef, '-40% 0px -50%') + useEffect(() => { + if (onScreen) { + didEnterView() + } + }, [onScreen]) + + return ( +
+
+
+

+ {currentVersion.versionTitle}.{patch.patchVersion} +

+ + {patch.release_candidate && ( + + Release Candidate + + )} + + {currentVersion.plan == 'enterprise-server' && ( + + Download + + )} + + +
+ +

{dayjs(patch.date).format('MMMM, DD, YYYY')}

+ + {patch.version !== latestPatch && currentVersion.currentRelease === latestRelease && ( +

+ {' '} + {t('notices.release_notes_use_latest')} +

+ )} + + {patch.version === latestPatch && currentVersion.currentRelease !== latestRelease && ( +

+ {' '} + {t('notices.release_notes_use_latest')} +

+ )} + + {patch.version !== latestPatch && currentVersion.currentRelease !== latestRelease && ( +

+ {' '} + {t('notices.release_notes_use_latest')} +

+ )} +
+ +
+
+ + +
+
+ ) +} diff --git a/components/release-notes/GHESReleaseNotes.tsx b/components/release-notes/GHESReleaseNotes.tsx new file mode 100644 index 0000000000..bf79b99c06 --- /dev/null +++ b/components/release-notes/GHESReleaseNotes.tsx @@ -0,0 +1,170 @@ +import { useState } from 'react' +import cx from 'classnames' +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, + LinkExternalIcon, +} from '@primer/octicons-react' +import { useMainContext } from 'components/context/MainContext' +import dayjs from 'dayjs' + +import { Link } from 'components/Link' +import { GHESReleaseNotesContextT } from './types' +import { GHESReleaseNotePatch } from './GHESReleaseNotePatch' + +type Props = { + context: GHESReleaseNotesContextT +} +export function GHESReleaseNotes({ context }: Props) { + const { currentLanguage, currentProduct } = useMainContext() + const [focusedPatch, setFocusedPatch] = useState('') + const { + prevRelease, + nextRelease, + latestPatch, + latestRelease, + currentVersion, + releaseNotes, + releases, + message, + } = context + return ( +
+
+
+ {prevRelease ? ( + + {prevRelease} + + ) : ( +
+ )} + +

+ {currentVersion.planTitle} {currentVersion.currentRelease} release notes +

+ + {nextRelease ? ( + + {nextRelease} + + ) : ( +
+ )} +
+ +
+ {releaseNotes.map((patch) => { + return ( + { + setFocusedPatch(patch.version) + }} + /> + ) + })} +
+
+ + +
+ ) +} diff --git a/components/release-notes/PatchNotes.tsx b/components/release-notes/PatchNotes.tsx new file mode 100644 index 0000000000..4aeda5d5bb --- /dev/null +++ b/components/release-notes/PatchNotes.tsx @@ -0,0 +1,84 @@ +import cx from 'classnames' +import slugger from 'github-slugger' +import { ReleaseNotePatch } from './types' +import { Link } from 'components/Link' + +const SectionToLabelMap: Record = { + features: 'Features', + bugs: 'Bug fixes', + known_issues: 'Known issues', + security_fixes: 'Security fixes', + changes: 'Changes', + deprecations: 'Deprecations', + backups: 'Backups', +} + +type Props = { + patch: ReleaseNotePatch + withReleaseNoteLabel?: boolean +} +export function PatchNotes({ patch, withReleaseNoteLabel }: Props) { + return ( + <> + {Object.entries(patch.sections).map(([key, sectionItems], i, arr) => { + const isLast = i === arr.length - 1 + return ( +
+ {withReleaseNoteLabel && ( +
+ + {SectionToLabelMap[key] || 'INVALID SECTION'} + +
+ )} +
    + {sectionItems.map((item) => { + if (typeof item === 'string') { + return ( +
  • + +
  • + ) + } + + const slug = item.heading ? slugger.slug(item.heading) : '' + return ( +
  • +

    + + {item.heading} + +

    + +
      + {item.notes.map((note) => { + return ( +
    • + ) + })} +
    +
  • + ) + })} +
+
+ ) + })} + + ) +} diff --git a/components/release-notes/types.ts b/components/release-notes/types.ts new file mode 100644 index 0000000000..398e4d5b54 --- /dev/null +++ b/components/release-notes/types.ts @@ -0,0 +1,49 @@ +export type CurrentVersion = { + plan: string + planTitle: string + versionTitle: string + currentRelease: string +} + +export type GHESMessage = { + ghes_release_notes_upgrade_patch_only: string + ghes_release_notes_upgrade_release_only: string + ghes_release_notes_upgrade_patch_and_release: string +} + +type ReleaseNoteSection = + | { + heading?: string + notes: Array + } + | string + +export type ReleaseNotePatch = { + patchVersion: string + version: string + downloadVersion: string + intro: string + date: string + friendlyDate: string + title: string + release_candidate?: boolean + currentWeek: boolean + sections: Record> +} + +export type GHAEReleaseNotesContextT = { + releaseNotes: Array + releases: Array<{ version: string; patches: Array }> + currentVersion: CurrentVersion +} + +export type GHESReleaseNotesContextT = { + latestPatch: string + prevRelease?: string + nextRelease?: string + latestRelease: string + currentVersion: CurrentVersion + releaseNotes: Array + releases: Array<{ version: string; patches: Array }> + message: GHESMessage +} diff --git a/data/ui.yml b/data/ui.yml index ddf09fe0e9..2fc1077a5e 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -20,9 +20,9 @@ header: early_access: 📣 Please do not share this URL publicly. This page contains content about an early access feature. release_notes_use_latest: Please use the latest release for the latest security, performance, and bug fixes. # GHES release notes - ghes_release_notes_upgrade_patch_only: 📣 This is not the latest patch release of Enterprise Server. {% data ui.header.notices.release_notes_use_latest %} - ghes_release_notes_upgrade_release_only: 📣 This is not the latest release of Enterprise Server. {% data ui.header.notices.release_notes_use_latest %} - ghes_release_notes_upgrade_patch_and_release: 📣 This is not the latest patch release of this release series, and this is not the latest release of Enterprise Server. {% data ui.header.notices.release_notes_use_latest %} + ghes_release_notes_upgrade_patch_only: 📣 This is not the latest patch release of Enterprise Server. + ghes_release_notes_upgrade_release_only: 📣 This is not the latest release of Enterprise Server. + ghes_release_notes_upgrade_patch_and_release: 📣 This is not the latest patch release of this release series, and this is not the latest release of Enterprise Server. release_notes: banner_text_current: These changes will roll out over the next one week. banner_text_past: These changes rolled out to enterprises during the week of diff --git a/includes/enterprise-server-release-notes.html b/includes/enterprise-server-release-notes.html index 88806054b0..9fadb7c7a6 100644 --- a/includes/enterprise-server-release-notes.html +++ b/includes/enterprise-server-release-notes.html @@ -54,15 +54,15 @@

{{ patch.date | date: "%B %d, %Y" }}

{% if patch.version != latestPatch and currentVersionObject.currentRelease == latestRelease %} -

{% data ui.header.notices.ghes_release_notes_upgrade_patch_only %}

+

{% data ui.header.notices.ghes_release_notes_upgrade_patch_only %} {% data ui.header.notices.release_notes_use_latest %}

{% endif %} {% if patch.version == latestPatch and currentVersionObject.currentRelease != latestRelease %} -

{% data ui.header.notices.ghes_release_notes_upgrade_release_only %}

+

{% data ui.header.notices.ghes_release_notes_upgrade_release_only %} {% data ui.header.notices.release_notes_use_latest %}

{% endif %} {% if patch.version != latestPatch and currentVersionObject.currentRelease != latestRelease %} -

{% data ui.header.notices.ghes_release_notes_upgrade_patch_and_release %}

+

{% data ui.header.notices.ghes_release_notes_upgrade_patch_and_release %} {% data ui.header.notices.release_notes_use_latest %}

{% endif %}
diff --git a/javascripts/release-notes.js b/javascripts/release-notes.js index 2ad3e49401..176b0321d1 100644 --- a/javascripts/release-notes.js +++ b/javascripts/release-notes.js @@ -1,4 +1,5 @@ export default function releaseNotes () { + if (window.next) return const patches = Array.from(document.querySelectorAll('.js-release-notes-patch')) if (patches.length === 0) return diff --git a/package-lock.json b/package-lock.json index 3eb369e1c1..817269afa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "@graphql-inspector/core": "^2.5.0", "@graphql-tools/load": "^6.2.8", "@octokit/rest": "^18.5.3", + "@types/github-slugger": "^1.3.0", "@types/lodash": "^4.14.169", "@types/react": "^17.0.6", "@types/react-dom": "^17.0.5", @@ -4040,6 +4041,12 @@ "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==", "devOptional": true }, + "node_modules/@types/github-slugger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz", + "integrity": "sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -27663,6 +27670,12 @@ "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==", "devOptional": true }, + "@types/github-slugger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz", + "integrity": "sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", diff --git a/package.json b/package.json index 443b40676c..d75336d655 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@graphql-inspector/core": "^2.5.0", "@graphql-tools/load": "^6.2.8", "@octokit/rest": "^18.5.3", + "@types/github-slugger": "^1.3.0", "@types/lodash": "^4.14.169", "@types/react": "^17.0.6", "@types/react-dom": "^17.0.5", diff --git a/pages/[versionId]/admin/release-notes.tsx b/pages/[versionId]/admin/release-notes.tsx new file mode 100644 index 0000000000..5de948f8d0 --- /dev/null +++ b/pages/[versionId]/admin/release-notes.tsx @@ -0,0 +1,81 @@ +import { GetServerSideProps } from 'next' +import { Liquid } from 'liquidjs' + +const liquid = new Liquid() + +import { + MainContextT, + MainContext, + getMainContextFromRequest, +} from 'components/context/MainContext' +import { DefaultLayout } from 'components/DefaultLayout' +import { GHAEReleaseNotes } from 'components/release-notes/GHAEReleaseNotes' +import { GHESReleaseNotes } from 'components/release-notes/GHESReleaseNotes' +import { + CurrentVersion, + GHAEReleaseNotesContextT, + GHESReleaseNotesContextT, +} from 'components/release-notes/types' + +type Props = { + mainContext: MainContextT + ghaeContext: GHAEReleaseNotesContextT + ghesContext: GHESReleaseNotesContextT + currentVersion: CurrentVersion +} +export default function ReleaseNotes({ + mainContext, + ghesContext, + ghaeContext, + currentVersion, +}: Props) { + return ( + + + {currentVersion.plan === 'enterprise-server' && } + + {currentVersion.plan === 'github-ae' && } + + + ) +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const req = context.req as any + const currentVersion = req.context.allVersions[req.context.currentVersion] + const { latestPatch = '', latestRelease = '' } = req.context + return { + props: { + mainContext: getMainContextFromRequest(req), + currentVersion, + ghesContext: { + currentVersion, + latestPatch, + latestRelease, + prevRelease: req.context.prevRelease || '', + nextRelease: req.context.nextRelease || '', + releaseNotes: req.context.releaseNotes, + releases: req.context.releases, + message: { + ghes_release_notes_upgrade_patch_only: liquid.parseAndRenderSync( + req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_patch_only, + { latestPatch, latestRelease } + ), + ghes_release_notes_upgrade_release_only: liquid.parseAndRenderSync( + req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_release_only, + { latestPatch, latestRelease } + ), + ghes_release_notes_upgrade_patch_and_release: liquid.parseAndRenderSync( + req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_patch_and_release, + { latestPatch, latestRelease } + ), + }, + }, + ghaeContext: { + currentVersion, + releaseNotes: req.context.releaseNotes, + releases: req.context.releases, + }, + }, + } +} diff --git a/tsconfig.json b/tsconfig.json index 6f0f8ed243..d994f47b99 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "isolatedModules": true, "jsx": "preserve", "baseUrl": ".", + "allowSyntheticDefaultImports": true }, "exclude": ["node_modules"], "include": ["*.d.ts", "**/*.ts", "**/*.tsx"]