diff --git a/src/helpers/get-github-repo-info.ts b/src/helpers/get-github-repo-info.ts index 355c8872..f75d41a8 100644 --- a/src/helpers/get-github-repo-info.ts +++ b/src/helpers/get-github-repo-info.ts @@ -10,9 +10,10 @@ const repoSidebarSectionSelectors = [ '.Layout-sidebar .BorderGrid-cell .hide-sm.hide-md', '.BorderGrid-cell > .hide-sm.hide-md', '.BorderGrid-cell .hide-sm.hide-md', + '.hide-sm.hide-md', ]; -const repoSidebarMarkerSelector = [ +export const repoSidebarMarkerSelector = [ 'a[href$="/stargazers"]', 'a[href$="/watchers"]', 'a[href$="/forks"]', @@ -69,7 +70,8 @@ export function getRepoSidebarSection() { export function getRepoSidebarBorderGrid() { const $sidebarSection = getRepoSidebarSection(); if ($sidebarSection.length > 0) { - return $sidebarSection.closest('.BorderGrid').first(); + const $borderGrid = $sidebarSection.closest('.BorderGrid').first(); + return $borderGrid.length > 0 ? $borderGrid : $sidebarSection.parent(); } const $borderGrids = $('.Layout-sidebar .BorderGrid, .BorderGrid') @@ -79,6 +81,27 @@ export function getRepoSidebarBorderGrid() { return pickFirstVisible($borderGrids); } +export function observeRepoSidebarReplacement(featureId: string, repoName: string, remount: () => void) { + const observerRoot = getRepoSidebarBorderGrid().parent()[0]; + if (!observerRoot) { + return; + } + + const observer = new MutationObserver(() => { + if (document.getElementById(featureId) || getRepoName() !== repoName) { + return; + } + + requestAnimationFrame(() => { + if (!document.getElementById(featureId) && getRepoName() === repoName) { + observer.disconnect(); + remount(); + } + }); + }); + observer.observe(observerRoot, { childList: true, subtree: true }); +} + export async function isRepoRoot() { return pageDetect.isRepoRoot(); } diff --git a/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx b/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx index f3b523b6..049642a7 100644 --- a/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx +++ b/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx @@ -6,8 +6,11 @@ import elementReady from 'element-ready'; import { getRepoName, getRepoSidebarBorderGrid, + getRepoSidebarSection, isPublicRepoWithMeta, isRepoRoot, + observeRepoSidebarReplacement, + repoSidebarMarkerSelector, } from '../../../../helpers/get-github-repo-info'; import { getActivity, getOpenrank } from '../../../../api/repo'; import { RepoMeta, metaStore } from '../../../../api/common'; @@ -38,19 +41,24 @@ const createTrendRow = () => { newBorderGridRow.id = featureId; newBorderGridRow.className = 'BorderGrid-row'; const newBorderGridCell = document.createElement('div'); - newBorderGridCell.className = 'BorderGrid-cell'; + newBorderGridCell.className = 'BorderGrid-cell hypercrx-repo-activity-openrank-trends-content'; newBorderGridRow.appendChild(newBorderGridCell); return { row: newBorderGridRow, cell: newBorderGridCell }; }; const mountTrendRow = (row: HTMLElement) => { - const $borderGrid = getRepoSidebarBorderGrid(); - if ($borderGrid.length === 0) { + const $sidebarContainer = getRepoSidebarBorderGrid(); + if ($sidebarContainer.length === 0) { return false; } - $borderGrid.append(row); + if (!$sidebarContainer.is('.BorderGrid')) { + row.className = getRepoSidebarSection().attr('class') || 'hide-sm hide-md'; + $(row).children().first().removeClass('BorderGrid-cell'); + } + + $sidebarContainer.append(row); return true; }; @@ -58,11 +66,20 @@ const init = async (): Promise => { platform = getPlatform(); repoName = getRepoName(); await getData(); - await elementReady('.BorderGrid, .BorderGrid-cell, .Layout-sidebar'); + await elementReady(repoSidebarMarkerSelector); const { row, cell } = createTrendRow(); renderTo(cell); - mountTrendRow(row); + if (!mountTrendRow(row)) { + return; + } + observeRepoSidebarReplacement(featureId, repoName, () => { + const { row, cell } = createTrendRow(); + renderTo(cell); + if (!mountTrendRow(row)) { + return; + } + }); }; const restore = async () => { @@ -73,8 +90,8 @@ const restore = async () => { await getData(); } // rerender the chart or it will be empty - await elementReady('.BorderGrid, .BorderGrid-cell, .Layout-sidebar'); - let container = $(`#${featureId}`).children('.BorderGrid-cell')[0]; + await elementReady(repoSidebarMarkerSelector); + let container = $(`#${featureId}`).children()[0]; if (!container) { const { row, cell } = createTrendRow(); diff --git a/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx b/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx index 5ac8a63d..e56a3f3b 100644 --- a/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx +++ b/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx @@ -1,6 +1,12 @@ import features from '../../../../feature-manager'; import elementReady from 'element-ready'; -import { getRepoName, getRepoSidebarSection, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; +import { + getRepoName, + getRepoSidebarSection, + isPublicRepoWithMeta, + observeRepoSidebarReplacement, + repoSidebarMarkerSelector, +} from '../../../../helpers/get-github-repo-info'; import { Label, RepoMeta, metaStore } from '../../../../api/common'; import { createRoot } from 'react-dom/client'; import OpenDiggerLabel from './OpenDiggerLabel'; @@ -10,7 +16,10 @@ import $ from 'jquery'; import isGithub from '../../../../helpers/is-github'; import { getPlatform } from '../../../../helpers/get-platform'; const featureId = features.getFeatureID(import.meta.url); +const markerId = `${featureId}-marker`; let platform: string; +let repoName: string; +let labels: Label[] = []; const getLabels = async (repoName: string) => { const meta = (await metaStore.get(platform, repoName)) as RepoMeta; // filtered all xxx-n and n is not 0 @@ -20,12 +29,16 @@ const getLabels = async (repoName: string) => { }; const renderTags = (labels: Label[]) => { + if (document.getElementById(markerId)) { + return; + } + const $sidebarSection = getRepoSidebarSection(); if ($sidebarSection.length === 0) { return; } - let githubTagContainer = $sidebarSection.find('.topic-tag.topic-tag-link').parent().first(); + let githubTagContainer = $sidebarSection.find('.topic-tag.topic-tag-link, a[href^="/topics/"]').parent().first(); // some repositories don't have tags, create a tag container for our tags if (githubTagContainer.length === 0) { githubTagContainer = $('
'); @@ -51,7 +64,6 @@ const renderTags = (labels: Label[]) => { } for (const label of labels) { const id = `opendigger-label-${label.id}`; - // if the tag already exists, skip if (document.getElementById(id)) { continue; } @@ -59,15 +71,21 @@ const renderTags = (labels: Label[]) => { createRoot(labelElement).render(); githubTagContainer.append(labelElement); } + + const marker = document.createElement('span'); + marker.id = markerId; + marker.hidden = true; + githubTagContainer.append(marker); }; const init = async (): Promise => { platform = getPlatform(); - const repoName = getRepoName(); - await elementReady('.BorderGrid-cell, .Layout-sidebar'); - const labels = await getLabels(repoName); + repoName = getRepoName(); + await elementReady(repoSidebarMarkerSelector); + labels = (await getLabels(repoName)) ?? []; if (labels && labels.length > 0) { renderTags(labels); + observeRepoSidebarReplacement(markerId, repoName, () => renderTags(labels)); } }; diff --git a/tests/e2e/content-scripts/repo-activity-openrank-trends.component.spec.ts b/tests/e2e/content-scripts/repo-activity-openrank-trends.component.spec.ts index 7e3ace46..fcd01ca5 100644 --- a/tests/e2e/content-scripts/repo-activity-openrank-trends.component.spec.ts +++ b/tests/e2e/content-scripts/repo-activity-openrank-trends.component.spec.ts @@ -28,6 +28,10 @@ test.describe('content script component: repo activity openrank trends', () => { // ECharts canvases should be rendered expect(await trendRow.locator('canvas').count()).toBeGreaterThan(0); + + // GitHub may replace its React-managed sidebar after the extension mounts. + await trendRow.evaluate((element) => element.remove()); + await expect(trendRow).toHaveCount(1); }); test('does not render on non-root repository pages', async () => { diff --git a/tests/unit/get-github-repo-info.test.ts b/tests/unit/get-github-repo-info.test.ts index afa5c730..9cac4b6c 100644 --- a/tests/unit/get-github-repo-info.test.ts +++ b/tests/unit/get-github-repo-info.test.ts @@ -35,6 +35,7 @@ import { getRepoName, getRepoNameByPage, getRepoNameByUrl, + getRepoSidebarBorderGrid, hasRepoContainerHeader, isPublicRepo, isPublicRepoWithMeta, @@ -69,6 +70,18 @@ describe('get-github-repo-info', () => { expect(hasRepoContainerHeader()).toBe(false); }); + it('finds the Primer repository sidebar without relying on generated class names', () => { + document.body.innerHTML = ` + + `; + + expect(getRepoSidebarBorderGrid().attr('data-testid')).toBe('primer-sidebar'); + }); + it('returns public repo status from meta and repo detection', async () => { document.head.innerHTML = ''; mocks.elementReady.mockResolvedValue(true);