-
Notifications
You must be signed in to change notification settings - Fork 5
feat(org-lens): org lens project detail page UI #1028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
91 commits
Select commit
Hold shift + click to select a range
d7883ac
feat(org-lens): scaffold project detail page, route & demo data
danoqualls 1dacc10
feat(org-lens): add project detail breadcrumb and hero
danoqualls c34092e
feat(org-lens): add our influence technical and ecosystem cards
danoqualls 60e67e4
feat(org-lens): add influence trend chart
danoqualls d70ba2f
feat(org-lens): add leaderboards tab with toggles and pagination
danoqualls 9fdd523
test(org-lens): add project detail e2e coverage
danoqualls 5698a9f
refactor(org-lens): align project detail with design + lfx patterns
danoqualls 25b1089
refactor(org-lens): two side-by-side leaderboards per prototype
danoqualls 7f7c910
refactor(org-lens): unify influence cards with trendlines per prototype
danoqualls 7ea46d5
refactor(org-lens): align project hero with My Org design
danoqualls 1d3d083
refactor(org-lens): drop 'Project' suffix from ecosystem scope label
danoqualls d11cc7b
feat(org-lens): full influence card set (5 technical, 9 ecosystem)
danoqualls ddfac97
fix(org-lens): show org name context and load without selected account
danoqualls c76b798
feat(org-lens): polish influence cards and sparkline tooltips
danoqualls 2198414
feat(org-lens): external HTML tooltip for sparkline cards
danoqualls c3bb528
feat(org-lens): hero cleanup, chip alignment, and card carousel arrows
danoqualls 3fac23a
fix(org-lens): floating tooltip and hero identity layout
danoqualls 678194d
fix(org-lens): move carousel arrows to section header row
danoqualls 0fc4f6f
feat(org-lens): match card style to app reference and smart arrow vis…
danoqualls 26f9671
feat(org-lens): vary chart types per metric and fix sparkline options…
danoqualls 6aff8bb
fix(org-lens): fix member ordering and nested ternary lint errors
danoqualls 87d7229
style(org-lens): match carousel arrow style to app.lfx.dev production
danoqualls bebe965
fix(org-lens): fix sparkline tooltip HTML and remove hover ring markers
danoqualls 4a213a5
style(org-lens): pill tabs and section header divider lines on projec…
danoqualls aa067c7
feat(org-lens): card detail drawer with trend chart and monthly break…
danoqualls 47a21a8
feat(org-lens): replace drawer chart with card-specific detail tables…
danoqualls 60f0dd9
fix(org-lens): move p-drawer header template outside @if block
danoqualls ee6725c
fix(org-lens): add COCOMO tooltip to software value info icon
danoqualls 98c9353
feat(org-lens): add paginator to influence leaderboard tables
danoqualls 59ecbf4
feat(org-lens): show org logos in influence leaderboard tables
danoqualls fdeab1a
feat(org-lens): stacked area trend chart with time range selector on …
danoqualls ada3eab
fix(org-lens): use fill: stack for stacked area trend chart in Chart.…
danoqualls 7dec43a
feat(org-lens): add tab bar component and restyle metric toggle
danoqualls b3b61bc
fix(org-lens): rework tab bar to match app.lfx.dev meetings page pattern
danoqualls 8db7012
feat(org-lens): move metric toggle into tab bar, normalize trend to 100%
danoqualls 9fbb1d9
fix(org-lens): tab bar layout, metric toggle placement, and chart pol…
danoqualls d6ada95
Potential fix for pull request finding
danoqualls 5c325d9
Potential fix for pull request finding
danoqualls a8a2329
Potential fix for pull request finding
danoqualls 4480a22
Potential fix for pull request finding
danoqualls 91bd0e7
Potential fix for pull request finding
danoqualls 1e047c4
Apply remaining changes
Copilot 4fdd6a0
fix(org-lens): wire project row handlers and fix broken /org/projects…
Copilot 8decc3c
fix(org-lens): address review comments — server route, handler wiring…
Copilot 5b6bbcc
fix(review): address PR #1028 review feedback
danoqualls 5e4f39b
Potential fix for pull request finding
danoqualls 6ff84af
Potential fix for pull request finding
danoqualls 6dec5a9
Potential fix for pull request finding
danoqualls 4b56539
fix(review): address PR #1028 review feedback
danoqualls efc3d5e
Potential fix for pull request finding
danoqualls 918b5c4
Potential fix for pull request finding
danoqualls 903f756
Potential fix for pull request finding
danoqualls e48e9b9
Potential fix for pull request finding
danoqualls 73998f6
Potential fix for pull request finding
danoqualls 7d2c38e
Potential fix for pull request finding
danoqualls d53e638
fix(review): address PR #1028 review feedback (continued)
danoqualls 49e0690
style(org-lens): fix prettier formatting on project detail files
danoqualls 2c1cadd
fix(org-lens): remove unused OrgLensProjectHealth import
danoqualls 74f99fe
fix(org-lens): address review 4593147828 findings
Copilot 8b0803c
fix(org-lens): address PR #1028 review feedback (round 2)
danoqualls 8d834c2
Merge branch 'main' into feat/LFXV2-1885
danoqualls dc2a616
Potential fix for pull request finding
danoqualls 8e6641e
Potential fix for pull request finding
danoqualls a978308
Potential fix for pull request finding
danoqualls b406405
Potential fix for pull request finding
danoqualls b2b39fc
Merge branch 'main' into feat/LFXV2-1885
danoqualls 200039d
fix(review): address PR #1028 review feedback (round 3)
danoqualls 30ea9cf
Merge branch 'main' into feat/LFXV2-1885
danoqualls 3300654
fix(review): address PR #1028 review feedback (round 4)
danoqualls 8aef932
style(org-lens): apply prettier formatting to project detail componen…
danoqualls 895a694
fix(review): address PR #1028 review feedback (round 5)
danoqualls 0eae6dd
fix(review): address PR #1028 review feedback (round 6)
danoqualls 752f6c0
Merge branch 'main' into feat/LFXV2-1885
danoqualls efbed98
Merge branch 'main' into feat/LFXV2-1885
danoqualls f675b40
Merge branch 'main' into feat/LFXV2-1885
danoqualls 1c3ad78
fix(review): guard SEEDS lookup against prototype pollution
danoqualls ab80b8a
Merge branch 'main' into feat/LFXV2-1885
danoqualls 2229321
Merge remote-tracking branch 'origin/feat/LFXV2-1885' into feat/LFXV2…
danoqualls a22a241
fix(review): point project detail breadcrumb/CTA to Projects list
danoqualls 2a44667
fix(review): scope constants, drop dead hero/trend fields, fix stale doc
danoqualls 4b0dc69
fix(review): fall back to demo org uid on project detail page
danoqualls 83c6b79
fix(review): revert demo-org fallback and add row aria-label
danoqualls 15b4d5d
fix(review): address PR #1028 review feedback from audigregorie
danoqualls 0c0a69f
Merge remote-tracking branch 'origin/main' into feat/LFXV2-1885
danoqualls 232083b
style(org-lens): fix prettier formatting in project detail component
danoqualls 3597e1a
fix(review): address PR #1028 Copilot review feedback
danoqualls 010c669
fix(review): address PR #1028 Copilot review feedback
danoqualls 7ad8323
fix(review): address PR #1028 Copilot review feedback
danoqualls 419b06c
Merge branch 'main' into feat/LFXV2-1885
MRashad26 1cc1b88
fix(review): address PR #1028 review feedback
danoqualls 7021c43
Merge branch 'main' into feat/LFXV2-1885
MRashad26 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // Copyright The Linux Foundation and each contributor to LFX. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| /** | ||
| * Org Lens · Project Detail Page E2E Tests (LFXV2-1885) | ||
| * | ||
| * Covers the acceptance criteria for the per-project Org Lens view: | ||
| * - data-testid resolution smoke test (breadcrumb, hero, tabs, card groups, trend) | ||
| * - tab strip switching (click + keyboard) with ?tab= URL persistence | ||
| * - leaderboard ranking, score/metric toggles with ?metric= persistence, | ||
| * Activity Count hiding the band tags, and Show more pagination | ||
| * - not-found (404) panel for an unknown slug | ||
| * | ||
| * Prerequisites: | ||
| * - Dev server running on the Playwright baseURL | ||
| * - User authenticated with the `org-lens-enabled` flag on and an organization selected | ||
| * | ||
| * Demo semantics (v1): the page is served from server-side demo fixtures | ||
| * (server/services/org-lens-project-detail.demo-data.ts). `kubernetes` is a rich seeded project; an | ||
| * unknown slug returns null → the not-found panel. | ||
| */ | ||
|
|
||
| import { expect, test } from '@playwright/test'; | ||
|
|
||
| const DETAIL_URL = '/org/projects/kubernetes'; | ||
| const DETAIL_URL_BOGUS = '/org/projects/totally-bogus-project'; | ||
| const DATA_LOAD_TIMEOUT = 30_000; | ||
|
|
||
| test.setTimeout(90_000); | ||
|
|
||
| test.describe('Org Project Detail — testid resolution', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto(DETAIL_URL, { waitUntil: 'domcontentloaded' }); | ||
| await expect(page).not.toHaveURL(/auth0\.com/); | ||
| await expect(page.getByTestId('project-detail-page')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT }); | ||
| }); | ||
|
|
||
| test('renders breadcrumb, hero and tab strip', async ({ page }) => { | ||
| await expect(page.getByTestId('project-detail-breadcrumb')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-hero')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-name')).toHaveText('Kubernetes'); | ||
| await expect(page.getByTestId('project-detail-first-commit')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-software-value')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-health-badge')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-foundation-pill')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-tab-pd-influence')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-tab-pd-leaderboards')).toBeVisible(); | ||
| }); | ||
|
|
||
| test('renders the Technical and Ecosystem card groups', async ({ page }) => { | ||
| for (const key of ['maintainers', 'contributors', 'commits', 'pull-requests']) { | ||
| await expect(page.getByTestId(`project-detail-technical-card-${key}`)).toBeVisible(); | ||
| } | ||
| for (const key of ['collaboration', 'meeting-attendance', 'board-members', 'committee-members']) { | ||
| await expect(page.getByTestId(`project-detail-ecosystem-card-${key}`)).toBeVisible(); | ||
| } | ||
|
|
||
| await page.getByTestId('project-detail-tab-pd-leaderboards').click(); | ||
| await expect(page.getByTestId('project-detail-trend-group')).toBeVisible(); | ||
| }); | ||
| }); | ||
|
|
||
| test.describe('Org Project Detail — tab strip', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto(DETAIL_URL, { waitUntil: 'domcontentloaded' }); | ||
| await expect(page.getByTestId('project-detail-page')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT }); | ||
| }); | ||
|
|
||
| test('defaults to Our Influence and switches to Leaderboards via click + URL', async ({ page }) => { | ||
| await expect(page.getByTestId('project-detail-technical-group')).toBeVisible(); | ||
| await page.getByTestId('project-detail-tab-pd-leaderboards').click(); | ||
| await expect(page).toHaveURL(/tab=pd-leaderboards/); | ||
| await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible(); | ||
| }); | ||
|
|
||
| test('deep-links to the Leaderboards tab via ?tab=', async ({ page }) => { | ||
| await page.goto(`${DETAIL_URL}?tab=pd-leaderboards`, { waitUntil: 'domcontentloaded' }); | ||
| await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT }); | ||
| }); | ||
|
Copilot marked this conversation as resolved.
Copilot marked this conversation as resolved.
|
||
|
|
||
| test('arrow keys move between tabs', async ({ page }) => { | ||
| await page.getByTestId('project-detail-tab-pd-influence').focus(); | ||
| await page.keyboard.press('ArrowRight'); | ||
| await expect(page.getByTestId('project-detail-tab-pd-leaderboards')).toBeFocused(); | ||
| }); | ||
| }); | ||
|
|
||
| test.describe('Org Project Detail — leaderboards', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto(`${DETAIL_URL}?tab=pd-leaderboards`, { waitUntil: 'domcontentloaded' }); | ||
| await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT }); | ||
| }); | ||
|
|
||
| test('renders both side-by-side boards with the viewing-org row pinned', async ({ page }) => { | ||
| await expect(page.getByTestId('project-detail-leaderboard-technical')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-leaderboard-ecosystem')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-leaderboard-technical-viewing-row')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-leaderboard-ecosystem-viewing-row')).toBeVisible(); | ||
| await expect(page.getByTestId('project-detail-trend-group')).toBeVisible(); | ||
| }); | ||
|
|
||
| test('metric toggle persists in the URL and switches the score column', async ({ page }) => { | ||
| await expect(page.getByRole('columnheader', { name: 'Influence Score' }).first()).toBeVisible(); | ||
| await page.getByTestId('project-detail-metric-activity').click(); | ||
| await expect(page).toHaveURL(/metric=activity/); | ||
| await expect(page.getByRole('columnheader', { name: 'Activity (12mo)' }).first()).toBeVisible(); | ||
| await expect(page.getByRole('columnheader', { name: 'Influence Score' })).toHaveCount(0); | ||
| }); | ||
|
danoqualls marked this conversation as resolved.
|
||
|
|
||
| test('time-range toggle persists in the URL and updates the activity column label', async ({ page }) => { | ||
| await page.getByTestId('project-detail-metric-activity').click(); | ||
| await page.getByTestId('project-detail-time-range-2y').click(); | ||
| await expect(page).toHaveURL(/range=2y/); | ||
| await expect(page.getByRole('columnheader', { name: 'Activity (24mo)' }).first()).toBeVisible(); | ||
| }); | ||
|
|
||
| test('search filters a board to matching organizations', async ({ page }) => { | ||
| await page.locator('[data-test="project-detail-search-technical"]').fill('Google'); | ||
| const rows = page.locator('[data-testid="project-detail-leaderboard-technical"] tbody tr'); | ||
| await expect(rows).toHaveCount(1); | ||
| await expect(rows.first()).toContainText('Google'); | ||
| }); | ||
| }); | ||
|
|
||
| test.describe('Org Project Detail — not found', () => { | ||
| test('renders the 404 panel for an unknown slug', async ({ page }) => { | ||
| await page.goto(DETAIL_URL_BOGUS, { waitUntil: 'domcontentloaded' }); | ||
| await expect(page).not.toHaveURL(/auth0\.com/); | ||
| await expect(page.getByTestId('project-detail-not-found')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT }); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
...c/app/modules/dashboards/org/org-project-detail/org-project-detail-tab-bar.component.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| <!-- Copyright The Linux Foundation and each contributor to LFX. --> | ||
| <!-- SPDX-License-Identifier: MIT --> | ||
|
|
||
| <!-- Left: tab pills --> | ||
| <div class="inline-flex items-center gap-1 rounded-full bg-gray-100 p-1" role="tablist" aria-label="Project detail sections" (keydown)="onTabKeydown($event)"> | ||
| @for (tab of tabs(); track tab.id) { | ||
| <button | ||
| #tabBtn | ||
| type="button" | ||
| role="tab" | ||
| [id]="'project-detail-tab-trigger-' + tab.id" | ||
| [attr.aria-selected]="activeTab() === tab.id" | ||
| [attr.aria-controls]="'project-detail-tab-panel-' + tab.id" | ||
| [attr.tabindex]="activeTab() === tab.id ? 0 : -1" | ||
| class="flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors" | ||
| [class.bg-blue-600]="activeTab() === tab.id" | ||
| [class.text-white]="activeTab() === tab.id" | ||
| [class.text-gray-600]="activeTab() !== tab.id" | ||
| [attr.data-testid]="'project-detail-tab-' + tab.id" | ||
| (click)="tabChange.emit(tab.id)"> | ||
| {{ tab.label }} | ||
| </button> | ||
| } | ||
| </div> | ||
|
|
||
| <!-- Right: optional metric toggle + time range dropdown --> | ||
| <div class="flex items-center gap-3"> | ||
| @if (metricOptions(); as opts) { | ||
| <div class="flex items-center gap-1.5" role="group" aria-label="Metric" data-testid="project-detail-metric-toggle"> | ||
| @for (opt of opts; track opt.id) { | ||
| <button | ||
| type="button" | ||
| class="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors" | ||
| [class.bg-blue-600]="metric() === opt.id" | ||
| [class.border-blue-600]="metric() === opt.id" | ||
| [class.text-white]="metric() === opt.id" | ||
| [class.bg-white]="metric() !== opt.id" | ||
| [class.border-gray-300]="metric() !== opt.id" | ||
| [class.text-gray-700]="metric() !== opt.id" | ||
| [attr.aria-pressed]="metric() === opt.id" | ||
| [attr.data-testid]="'project-detail-metric-' + opt.id" | ||
| (click)="metricChange.emit(opt.id)"> | ||
| <i [class]="opt.icon" aria-hidden="true"></i> | ||
| {{ opt.label }} | ||
| </button> | ||
| } | ||
| </div> | ||
| } | ||
| <div class="flex items-center gap-1 rounded-full bg-gray-100 p-1" role="group" aria-label="Time range" data-testid="project-detail-time-range"> | ||
| @for (opt of timeRangeOptions(); track opt.id) { | ||
| <button | ||
| type="button" | ||
| class="rounded-full px-3 py-1 text-sm font-medium transition-colors" | ||
| [class.bg-white]="timeRange() === opt.id" | ||
| [class.text-gray-900]="timeRange() === opt.id" | ||
| [class.shadow-sm]="timeRange() === opt.id" | ||
| [class.text-gray-600]="timeRange() !== opt.id" | ||
| [attr.aria-pressed]="timeRange() === opt.id" | ||
| [attr.data-testid]="'project-detail-time-range-' + opt.id" | ||
| (click)="timeRangeChange.emit(opt.id)"> | ||
| {{ opt.label }} | ||
| </button> | ||
| } | ||
| </div> | ||
| </div> |
49 changes: 49 additions & 0 deletions
49
...src/app/modules/dashboards/org/org-project-detail/org-project-detail-tab-bar.component.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // Copyright The Linux Foundation and each contributor to LFX. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| import { isPlatformBrowser } from '@angular/common'; | ||
| import { Component, ElementRef, inject, input, output, PLATFORM_ID, viewChildren } from '@angular/core'; | ||
| import type { OrgLensLeaderboardMetric, OrgLensLeaderboardTimeRange, OrgLensProjectDetailTab } from '@lfx-one/shared/interfaces'; | ||
|
|
||
| @Component({ | ||
| selector: 'lfx-org-project-detail-tab-bar', | ||
| host: { | ||
| class: 'flex w-full items-center justify-between gap-4 rounded-xl border border-gray-200 bg-white px-4 py-2.5 shadow-sm', | ||
| 'data-testid': 'project-detail-controls-bar', | ||
| }, | ||
| templateUrl: './org-project-detail-tab-bar.component.html', | ||
| }) | ||
| export class OrgProjectDetailTabBarComponent { | ||
| private readonly tabBtns = viewChildren<ElementRef<HTMLButtonElement>>('tabBtn'); | ||
|
|
||
| private readonly platformId = inject(PLATFORM_ID); | ||
|
|
||
| public readonly tabs = input.required<{ id: OrgLensProjectDetailTab; label: string }[]>(); | ||
| public readonly activeTab = input.required<OrgLensProjectDetailTab>(); | ||
| public readonly timeRange = input.required<OrgLensLeaderboardTimeRange>(); | ||
| public readonly timeRangeOptions = input.required<{ id: OrgLensLeaderboardTimeRange; label: string }[]>(); | ||
| /** When provided, renders the metric toggle between the tab pills and the time dropdown. */ | ||
| public readonly metric = input<OrgLensLeaderboardMetric | null>(null); | ||
| public readonly metricOptions = input<{ id: OrgLensLeaderboardMetric; label: string; icon: string }[] | null>(null); | ||
| public readonly tabChange = output<OrgLensProjectDetailTab>(); | ||
| public readonly timeRangeChange = output<OrgLensLeaderboardTimeRange>(); | ||
| public readonly metricChange = output<OrgLensLeaderboardMetric>(); | ||
|
|
||
| protected onTabKeydown(event: KeyboardEvent): void { | ||
| const ids = this.tabs().map((t) => t.id); | ||
| const idx = ids.indexOf(this.activeTab()); | ||
| let next: number | null = null; | ||
| if (event.key === 'ArrowRight') next = (idx + 1) % ids.length; | ||
| else if (event.key === 'ArrowLeft') next = (idx - 1 + ids.length) % ids.length; | ||
| else if (event.key === 'Home') next = 0; | ||
| else if (event.key === 'End') next = ids.length - 1; | ||
| if (next !== null) { | ||
| event.preventDefault(); | ||
| this.tabChange.emit(ids[next]); | ||
| if (isPlatformBrowser(this.platformId)) { | ||
| const n = next; | ||
| setTimeout(() => this.tabBtns()[n]?.nativeElement.focus()); | ||
| } | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.