diff --git a/src/cards/repo.js b/src/cards/repo.js index a9c2afc38a222..b9214032af8a3 100644 --- a/src/cards/repo.js +++ b/src/cards/repo.js @@ -66,6 +66,13 @@ const renderRepoCard = (repo, options = {}) => { } = repo; const { hide_border = false, + hide_title = false, + hide_text = false, + stats_only = false, + show_issues = false, + show_prs = false, + show_age = false, + age_metric = "first", title_color, icon_color, text_color, @@ -82,6 +89,8 @@ const renderRepoCard = (repo, options = {}) => { const header = show_owner ? nameWithOwner : name; const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified"; const langColor = (primaryLanguage && primaryLanguage.color) || "#333"; + const shouldHideTitle = stats_only || hide_title; + const shouldHideText = stats_only || hide_text; const descriptionMaxLines = description_lines_count ? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES) : DESCRIPTION_MAX_LINES; @@ -100,9 +109,9 @@ const renderRepoCard = (repo, options = {}) => { .map((line) => `${encodeHTML(line)}`) .join(""); + const effectiveLines = shouldHideText ? 0 : descriptionLinesCount; const height = - (descriptionLinesCount > 1 ? 120 : 110) + - descriptionLinesCount * lineHeight; + (effectiveLines > 1 ? 120 : 110) + effectiveLines * lineHeight; const i18n = new I18n({ locale, @@ -138,13 +147,114 @@ const renderRepoCard = (repo, options = {}) => { ICON_SIZE, ); + const issuesCountRaw = + typeof repo.openIssuesCount === "number" + ? repo.openIssuesCount + : repo?.issues?.totalCount; + const prsCountRaw = + typeof repo.openPrsCount === "number" + ? repo.openPrsCount + : repo?.pullRequests?.totalCount; + + const issuesCount = + typeof issuesCountRaw === "number" ? issuesCountRaw : undefined; + const prsCount = typeof prsCountRaw === "number" ? prsCountRaw : undefined; + const issuesLabel = + issuesCount !== undefined ? `${kFormatter(issuesCount)}` : null; + const prsLabel = prsCount !== undefined ? `${kFormatter(prsCount)}` : null; + + const svgIssues = + show_issues && issuesLabel !== null + ? iconWithLabel(icons.issues, issuesLabel, "issues", ICON_SIZE) + : ""; + const svgPRs = + show_prs && prsLabel !== null + ? iconWithLabel(icons.prs, prsLabel, "prs", ICON_SIZE) + : ""; + + const resolveAgeDate = () => { + if (age_metric === "created") { + return repo.createdAt; + } + if (age_metric === "pushed") { + return repo.pushedAt; + } + return repo.firstCommitDate || repo.createdAt || repo.pushedAt; + }; + + const renderAgeBadge = () => { + const iso = resolveAgeDate(); + if (!show_age || !iso) { + return { svg: "", label: "" }; + } + const then = new Date(iso).getTime(); + const now = Date.now(); + const diff = Math.max(0, now - then); + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + const month = 30 * day; + const year = 365 * day; + let label; + if (diff >= year) { + const years = Math.floor(diff / year); + label = `${years}y`; + } else if (diff >= month) { + const months = Math.floor(diff / month); + label = `${months}mo`; + } else if (diff >= day) { + const days = Math.floor(diff / day); + label = `${days}d`; + } else if (diff >= hour) { + const hours = Math.floor(diff / hour); + label = `${hours}h`; + } else if (diff >= minute) { + const minutes = Math.floor(diff / minute); + label = `${minutes}m`; + } else { + const seconds = Math.floor(diff / 1000); + label = `${seconds}s`; + } + return { + svg: iconWithLabel(icons.commits, label, "age", ICON_SIZE), + label, + }; + }; + + const ageBadge = renderAgeBadge(); + + const statsItems = []; + const statsSizes = []; + + if (svgLanguage) { + statsItems.push(svgLanguage); + statsSizes.push(measureText(langName, 12)); + } + + statsItems.push(svgStars); + statsSizes.push(ICON_SIZE + measureText(`${totalStars}`, 12)); + + statsItems.push(svgForks); + statsSizes.push(ICON_SIZE + measureText(`${totalForks}`, 12)); + + if (svgIssues && issuesLabel !== null) { + statsItems.push(svgIssues); + statsSizes.push(ICON_SIZE + measureText(issuesLabel, 12)); + } + + if (svgPRs && prsLabel !== null) { + statsItems.push(svgPRs); + statsSizes.push(ICON_SIZE + measureText(prsLabel, 12)); + } + + if (ageBadge.svg) { + statsItems.push(ageBadge.svg); + statsSizes.push(ICON_SIZE + measureText(ageBadge.label, 12)); + } + const starAndForkCount = flexLayout({ - items: [svgLanguage, svgStars, svgForks], - sizes: [ - measureText(langName, 12), - ICON_SIZE + measureText(`${totalStars}`, 12), - ICON_SIZE + measureText(`${totalForks}`, 12), - ], + items: statsItems, + sizes: statsSizes, gap: 25, }).join(""); @@ -159,7 +269,7 @@ const renderRepoCard = (repo, options = {}) => { card.disableAnimations(); card.setHideBorder(hide_border); - card.setHideTitle(false); + card.setHideTitle(shouldHideTitle); card.setCSS(` .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} } .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} } @@ -179,9 +289,15 @@ const renderRepoCard = (repo, options = {}) => { : "" } + ${ + shouldHideText + ? "" + : ` ${descriptionSvg} + ` + } ${starAndForkCount} diff --git a/tests/ops.test.js b/tests/ops.test.js index dd4e08b88c3bc..0a94accfd5f1f 100644 --- a/tests/ops.test.js +++ b/tests/ops.test.js @@ -32,6 +32,7 @@ describe("Test ops.js", () => { expect(parseArray("a,b,c")).toEqual(["a", "b", "c"]); expect(parseArray("a, b, c")).toEqual(["a", " b", " c"]); // preserves spaces expect(parseArray("")).toEqual([]); + expect(parseArray(["x", "y"])).toEqual(["x", "y"]); // @ts-ignore expect(parseArray(undefined)).toEqual([]); }); diff --git a/tests/renderRepoCard.test.js b/tests/renderRepoCard.test.js index 9fb5ab36c90de..da77a529f421e 100644 --- a/tests/renderRepoCard.test.js +++ b/tests/renderRepoCard.test.js @@ -368,4 +368,34 @@ describe("Test renderRepoCard", () => { ); expect(document.querySelector("svg")).toHaveAttribute("height", "120"); }); + + it("should render issues and PR badges when enabled", () => { + const repoWithCounts = { + ...data_repo.repository, + openIssuesCount: 7, + openPrsCount: 3, + }; + document.body.innerHTML = renderRepoCard(repoWithCounts, { + show_issues: true, + show_prs: true, + }); + + expect(queryByTestId(document.body, "issues")).toHaveTextContent("7"); + expect(queryByTestId(document.body, "prs")).toHaveTextContent("3"); + }); + + it("should fall back to GraphQL connections when counts are missing", () => { + const repoWithConnections = { + ...data_repo.repository, + issues: { totalCount: 5 }, + pullRequests: { totalCount: 2 }, + }; + document.body.innerHTML = renderRepoCard(repoWithConnections, { + show_issues: true, + show_prs: true, + }); + + expect(queryByTestId(document.body, "issues")).toHaveTextContent("5"); + expect(queryByTestId(document.body, "prs")).toHaveTextContent("2"); + }); });