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");
+ });
});