add items and options to repo card, validate input, orgs->owners
This commit is contained in:
+19
-2
@@ -14,7 +14,7 @@ export default async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
repos,
|
||||
orgs,
|
||||
owners,
|
||||
hide,
|
||||
hide_title,
|
||||
hide_border,
|
||||
@@ -67,10 +67,27 @@ export default async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
const safePattern = /^[\w\/.]+$/;
|
||||
if (
|
||||
(username && !safePattern.test(username)) ||
|
||||
(repos && !safePattern.test(repos)) ||
|
||||
(owners && !safePattern.test(owners))
|
||||
) {
|
||||
return res.send(
|
||||
renderError("Something went wrong", "Username, repository or owner contains unsafe characters", {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const showStats = parseArray(show);
|
||||
const repositories=parseArray(repos);
|
||||
const organizations=parseArray(orgs);
|
||||
const organizations=parseArray(owners);
|
||||
const stats = await fetchStats(
|
||||
username,
|
||||
parseBoolean(include_all_commits),
|
||||
|
||||
+38
-2
@@ -2,7 +2,7 @@ import { renderRepoCard } from "../src/cards/repo-card.js";
|
||||
import { blacklist } from "../src/common/blacklist.js";
|
||||
import {
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
CONSTANTS, parseArray,
|
||||
parseBoolean,
|
||||
renderError,
|
||||
} from "../src/common/utils.js";
|
||||
@@ -20,6 +20,11 @@ export default async (req, res) => {
|
||||
bg_color,
|
||||
theme,
|
||||
show_owner,
|
||||
show,
|
||||
show_icons,
|
||||
number_format,
|
||||
text_bold,
|
||||
line_height,
|
||||
cache_seconds,
|
||||
locale,
|
||||
border_radius,
|
||||
@@ -53,8 +58,33 @@ export default async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
const safePattern = /^[\w\/.]+$/;
|
||||
if (
|
||||
(username && !safePattern.test(username)) ||
|
||||
(repos && !safePattern.test(repo))
|
||||
) {
|
||||
return res.send(
|
||||
renderError("Something went wrong", "Username or repository contains unsafe characters", {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const repoData = await fetchRepo(username, repo);
|
||||
const showStats = parseArray(show);
|
||||
const repoData = await fetchRepo(
|
||||
username,
|
||||
repo,
|
||||
showStats.includes("prs_authored"),
|
||||
showStats.includes("prs_commented"),
|
||||
showStats.includes("prs_reviewed"),
|
||||
showStats.includes("issues_authored"),
|
||||
showStats.includes("issues_commented"),
|
||||
);
|
||||
|
||||
let cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.PIN_CARD_CACHE_SECONDS, 10),
|
||||
@@ -81,6 +111,12 @@ export default async (req, res) => {
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
show: showStats,
|
||||
show_icons,
|
||||
number_format,
|
||||
text_bold,
|
||||
line_height,
|
||||
username,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
description_lines_count,
|
||||
}),
|
||||
|
||||
+92
-3
@@ -12,9 +12,10 @@ import {
|
||||
wrapTextMultiline,
|
||||
iconWithLabel,
|
||||
createLanguageNode,
|
||||
clampValue,
|
||||
clampValue, buildSearchFilter,
|
||||
} from "../common/utils.js";
|
||||
import { repoCardLocales } from "../translations.js";
|
||||
import {createTextNode} from "./stats-card.js";
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
const DESCRIPTION_LINE_WIDTH = 59;
|
||||
@@ -64,6 +65,11 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
isTemplate,
|
||||
starCount,
|
||||
forkCount,
|
||||
totalPRsAuthored,
|
||||
totalPRsCommented,
|
||||
totalPRsReviewed,
|
||||
totalIssuesAuthored,
|
||||
totalIssuesCommented,
|
||||
} = repo;
|
||||
const {
|
||||
hide_border = false,
|
||||
@@ -72,6 +78,12 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner = false,
|
||||
show = [],
|
||||
show_icons,
|
||||
number_format,
|
||||
text_bold,
|
||||
line_height = 10,
|
||||
username,
|
||||
theme = "default_repocard",
|
||||
border_radius,
|
||||
border_color,
|
||||
@@ -79,6 +91,73 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
description_lines_count,
|
||||
} = options;
|
||||
|
||||
let repoFilter = buildSearchFilter([nameWithOwner], []);
|
||||
const STATS = {};
|
||||
if (show.includes("prs_authored")) {
|
||||
STATS.prs_authored = {
|
||||
icon: icons.prs,
|
||||
label: i18n.t("repocard.prs-authored"),
|
||||
value: totalPRsAuthored,
|
||||
id: "prs_authored",
|
||||
link: `https://github.com/search?q=${repoFilter}author%3A${username}&type=pullrequests`,
|
||||
};
|
||||
}
|
||||
if (show.includes("prs_commented")) {
|
||||
STATS.prs_commented = {
|
||||
icon: icons.comments,
|
||||
label: i18n.t("repocard.prs-commented"),
|
||||
value: totalPRsCommented,
|
||||
id: "prs_commented",
|
||||
link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&type=pullrequests`,
|
||||
};
|
||||
}
|
||||
if (show.includes("prs_reviewed")) {
|
||||
STATS.prs_reviewed = {
|
||||
icon: icons.reviews,
|
||||
label: i18n.t("repocard.prs-reviewed"),
|
||||
value: totalPRsReviewed,
|
||||
id: "prs_reviewed",
|
||||
link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${username}+-author%3A${username}&type=pullrequests`,
|
||||
};
|
||||
}
|
||||
if (show.includes("issues_authored")) {
|
||||
STATS.issues_authored = {
|
||||
icon: icons.issues,
|
||||
label: i18n.t("repocard.issues-authored"),
|
||||
value: totalIssuesAuthored,
|
||||
id: "issues_authored",
|
||||
link: `https://github.com/search?q=${repoFilter}author%3A${username}&type=issues`,
|
||||
};
|
||||
}
|
||||
if (show.includes("issues_commented")) {
|
||||
STATS.issues_commented = {
|
||||
icon: icons.discussions_started,
|
||||
label: i18n.t("repocard.issues-commented"),
|
||||
value: totalIssuesCommented,
|
||||
id: "issues_commented",
|
||||
link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&type=issues`,
|
||||
};
|
||||
}
|
||||
|
||||
const statItems = Object.keys(STATS)
|
||||
.map((key, index) =>
|
||||
// create the text nodes, and pass index so that we can calculate the line spacing
|
||||
createTextNode({
|
||||
icon: STATS[key].icon,
|
||||
label: STATS[key].label,
|
||||
value: STATS[key].value,
|
||||
id: STATS[key].id,
|
||||
unitSymbol: STATS[key].unitSymbol,
|
||||
index,
|
||||
showIcons: show_icons,
|
||||
shiftValuePos: 29.01,
|
||||
bold: text_bold,
|
||||
number_format,
|
||||
link: STATS[key].link,
|
||||
}),
|
||||
);
|
||||
|
||||
const extraLHeight = parseInt(String(line_height), 10);
|
||||
const lineHeight = 10;
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
|
||||
@@ -101,9 +180,11 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
|
||||
.join("");
|
||||
|
||||
const extraHeight=45 + (statItems.length + 1) * extraLHeight;
|
||||
const height =
|
||||
(descriptionLinesCount > 1 ? 120 : 110) +
|
||||
descriptionLinesCount * lineHeight;
|
||||
descriptionLinesCount * lineHeight
|
||||
+ extraHeight;
|
||||
|
||||
const i18n = new I18n({
|
||||
locale,
|
||||
@@ -184,9 +265,17 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
${descriptionSvg}
|
||||
</text>
|
||||
|
||||
<g transform="translate(30, ${height - 75})">
|
||||
<g transform="translate(30, ${height - 75 - extraHeight})">
|
||||
${starAndForkCount}
|
||||
</g>
|
||||
|
||||
<svg x="0" y="0">
|
||||
${flexLayout({
|
||||
items: statItems,
|
||||
gap: extraLHeight,
|
||||
direction: "column",
|
||||
}).join("")}
|
||||
</svg>
|
||||
`);
|
||||
};
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ const getStyles = ({
|
||||
* @param {Partial<StatCardOptions>} options The card options.
|
||||
* @returns {string} The stats card SVG object.
|
||||
*/
|
||||
const renderStatsCard = (stats, options = {}, username, repos=[], orgs=[]) => {
|
||||
const renderStatsCard = (stats, options = {}, username, repos=[], owners=[]) => {
|
||||
const {
|
||||
name,
|
||||
totalStars,
|
||||
@@ -351,7 +351,7 @@ const renderStatsCard = (stats, options = {}, username, repos=[], orgs=[]) => {
|
||||
};
|
||||
}
|
||||
|
||||
let repoFilter = buildSearchFilter(repos, orgs);
|
||||
let repoFilter = buildSearchFilter(repos, owners);
|
||||
if (show.includes("prs_authored")) {
|
||||
STATS.prs_authored = {
|
||||
icon: icons.prs,
|
||||
@@ -605,5 +605,5 @@ const renderStatsCard = (stats, options = {}, username, repos=[], orgs=[]) => {
|
||||
`);
|
||||
};
|
||||
|
||||
export { renderStatsCard };
|
||||
export { renderStatsCard, createTextNode };
|
||||
export default renderStatsCard;
|
||||
|
||||
+3
-3
@@ -217,14 +217,14 @@ const fallbackColor = (color, fallbackColor) => {
|
||||
);
|
||||
};
|
||||
|
||||
const buildSearchFilter = (repos = [], orgs = []) => {
|
||||
const buildSearchFilter = (repos = [], owners = []) => {
|
||||
let repoFilter =
|
||||
Array.isArray(repos) && repos.length > 0
|
||||
? repos.map((r) => `repo%3A${encodeURIComponent(r)}`).join("+") + "+"
|
||||
: "";
|
||||
let orgFilter =
|
||||
Array.isArray(orgs) && orgs.length > 0
|
||||
? orgs.map((o) => `org%3A${encodeURIComponent(o)}`).join("+") + "+"
|
||||
Array.isArray(owners) && owners.length > 0
|
||||
? owners.map((o) => `owner%3A${encodeURIComponent(o)}`).join("+") + "+"
|
||||
: "";
|
||||
return repoFilter + orgFilter;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
import { retryer } from "../common/retryer.js";
|
||||
import { MissingParamError, request } from "../common/utils.js";
|
||||
import { fetchRepoUserStats } from "./stats-fetcher.js";
|
||||
|
||||
/**
|
||||
* @typedef {import('axios').AxiosRequestHeaders} AxiosRequestHeaders Axios request headers.
|
||||
@@ -69,7 +70,12 @@ const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME";
|
||||
* @param {string} reponame GitHub repository name.
|
||||
* @returns {Promise<RepositoryData>} Repository data.
|
||||
*/
|
||||
const fetchRepo = async (username, reponame) => {
|
||||
const fetchRepo = async (username, reponame, include_prs_authored = false,
|
||||
include_prs_commented = false,
|
||||
include_prs_reviewed = false,
|
||||
include_issues_authored = false,
|
||||
include_issues_commented = false,
|
||||
) => {
|
||||
if (!username && !reponame) {
|
||||
throw new MissingParamError(["username", "repo"], urlExample);
|
||||
}
|
||||
@@ -95,7 +101,18 @@ const fetchRepo = async (username, reponame) => {
|
||||
if (!data.user.repository || data.user.repository.isPrivate) {
|
||||
throw new Error("User Repository Not found");
|
||||
}
|
||||
let repoUserStats = await fetchRepoUserStats(
|
||||
username,
|
||||
reponame,
|
||||
[],
|
||||
include_prs_authored,
|
||||
include_prs_commented,
|
||||
include_prs_reviewed,
|
||||
include_issues_authored,
|
||||
include_issues_commented,
|
||||
);
|
||||
return {
|
||||
...repoUserStats,
|
||||
...data.user.repository,
|
||||
starCount: data.user.repository.stargazers.totalCount,
|
||||
};
|
||||
@@ -108,7 +125,18 @@ const fetchRepo = async (username, reponame) => {
|
||||
) {
|
||||
throw new Error("Organization Repository Not found");
|
||||
}
|
||||
let repoUserStats = await fetchRepoUserStats(
|
||||
username,
|
||||
reponame,
|
||||
[],
|
||||
include_prs_authored,
|
||||
include_prs_commented,
|
||||
include_prs_reviewed,
|
||||
include_issues_authored,
|
||||
include_issues_commented,
|
||||
);
|
||||
return {
|
||||
...repoUserStats,
|
||||
...data.organization.repository,
|
||||
starCount: data.organization.repository.stargazers.totalCount,
|
||||
};
|
||||
|
||||
@@ -168,7 +168,7 @@ const statsFetcher = async ({
|
||||
* @description Done like this because the GitHub API does not provide a way to fetch all the commits. See
|
||||
* #92#issuecomment-661026467 and #211 for more information.
|
||||
*/
|
||||
const totalItemsFetcher = async (username, repos, orgs, type, filter) => {
|
||||
const totalItemsFetcher = async (username, repos, owners, type, filter) => {
|
||||
if (!githubUsernameRegex.test(username)) {
|
||||
logger.log("Invalid username provided.");
|
||||
throw new Error("Invalid username provided.");
|
||||
@@ -182,7 +182,7 @@ const totalItemsFetcher = async (username, repos, orgs, type, filter) => {
|
||||
`https://api.github.com/search/` +
|
||||
type +
|
||||
`?per_page=1&q=` +
|
||||
buildSearchFilter(variables.repos, variables.orgs)+
|
||||
buildSearchFilter(variables.repos, variables.owners)+
|
||||
filter,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -194,7 +194,7 @@ const totalItemsFetcher = async (username, repos, orgs, type, filter) => {
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await retryer(fetchTotalItems, { login: username, repos, orgs });
|
||||
res = await retryer(fetchTotalItems, { login: username, repos, owners });
|
||||
} catch (err) {
|
||||
logger.log(err);
|
||||
throw new Error(err);
|
||||
@@ -210,6 +210,65 @@ const totalItemsFetcher = async (username, repos, orgs, type, filter) => {
|
||||
return totalCount;
|
||||
};
|
||||
|
||||
const fetchRepoUserStats = async (
|
||||
username,
|
||||
repos,
|
||||
owners,
|
||||
include_prs_authored,
|
||||
include_prs_commented,
|
||||
include_prs_reviewed,
|
||||
include_issues_authored,
|
||||
include_issues_commented
|
||||
) => {
|
||||
let stats = {};
|
||||
if (include_prs_authored) {
|
||||
stats.totalPRsAuthored = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
owners,
|
||||
"issues",
|
||||
`author:${username}+type:pr`,
|
||||
);
|
||||
}
|
||||
if (include_prs_commented) {
|
||||
stats.totalPRsCommented = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
owners,
|
||||
"issues",
|
||||
`commenter:${username}+-author:${username}+type:pr`,
|
||||
);
|
||||
}
|
||||
if (include_prs_reviewed) {
|
||||
stats.totalPRsReviewed = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
owners,
|
||||
"issues",
|
||||
`reviewed-by:${username}+-author:${username}+type:pr`,
|
||||
);
|
||||
}
|
||||
if (include_issues_authored) {
|
||||
stats.totalIssuesAuthored = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
owners,
|
||||
"issues",
|
||||
`author:${username}+type:issue`,
|
||||
);
|
||||
}
|
||||
if (include_issues_commented) {
|
||||
stats.totalIssuesCommented = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
owners,
|
||||
"issues",
|
||||
`commenter:${username}+-author:${username}+type:issue`,
|
||||
);
|
||||
}
|
||||
return stats;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import("./types").StatsData} StatsData Stats data.
|
||||
*/
|
||||
@@ -233,7 +292,7 @@ const fetchStats = async (
|
||||
include_discussions = false,
|
||||
include_discussions_answers = false,
|
||||
repos=[],
|
||||
orgs=[],
|
||||
owners=[],
|
||||
include_prs_authored = false,
|
||||
include_prs_commented = false,
|
||||
include_prs_reviewed = false,
|
||||
@@ -301,58 +360,24 @@ const fetchStats = async (
|
||||
stats.totalCommits = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
orgs,
|
||||
owners,
|
||||
"commits",
|
||||
`author:${username}`,
|
||||
);
|
||||
} else {
|
||||
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
|
||||
}
|
||||
if (include_prs_authored) {
|
||||
stats.totalPRsAuthored = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
orgs,
|
||||
"issues",
|
||||
`author:${username}+type:pr`,
|
||||
);
|
||||
}
|
||||
if (include_prs_commented) {
|
||||
stats.totalPRsCommented = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
orgs,
|
||||
"issues",
|
||||
`commenter:${username}+-author:${username}+type:pr`,
|
||||
);
|
||||
}
|
||||
if (include_prs_reviewed) {
|
||||
stats.totalPRsReviewed = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
orgs,
|
||||
"issues",
|
||||
`reviewed-by:${username}+-author:${username}+type:pr`,
|
||||
);
|
||||
}
|
||||
if (include_issues_authored) {
|
||||
stats.totalIssuesAuthored = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
orgs,
|
||||
"issues",
|
||||
`author:${username}+type:issue`,
|
||||
);
|
||||
}
|
||||
if (include_issues_commented) {
|
||||
stats.totalIssuesCommented = await totalItemsFetcher(
|
||||
username,
|
||||
repos,
|
||||
orgs,
|
||||
"issues",
|
||||
`commenter:${username}+-author:${username}+type:issue`,
|
||||
);
|
||||
}
|
||||
let repoUserStats = await fetchRepoUserStats(
|
||||
username,
|
||||
repos,
|
||||
owners,
|
||||
include_prs_authored,
|
||||
include_prs_commented,
|
||||
include_prs_reviewed,
|
||||
include_issues_authored,
|
||||
include_issues_commented
|
||||
);
|
||||
Object.assign(stats, repoUserStats);
|
||||
|
||||
stats.totalPRs = user.pullRequests.totalCount;
|
||||
if (include_merged_pull_requests) {
|
||||
@@ -397,5 +422,5 @@ const fetchStats = async (
|
||||
return stats;
|
||||
};
|
||||
|
||||
export { fetchStats };
|
||||
export { fetchStats, fetchRepoUserStats };
|
||||
export default fetchStats;
|
||||
|
||||
@@ -340,6 +340,21 @@ const statCardLocales = ({ name, apostrophe }) => {
|
||||
"statcard.issues-commented": {
|
||||
en: "Issues Commented",
|
||||
},
|
||||
"repocard.prs-authored": {
|
||||
en: "my Created PRs",
|
||||
},
|
||||
"repocard.prs-commented": {
|
||||
en: "my Commented PRs",
|
||||
},
|
||||
"repocard.prs-reviewed": {
|
||||
en: "my Reviewed PRs",
|
||||
},
|
||||
"repocard.issues-authored": {
|
||||
en: "my Created Issues",
|
||||
},
|
||||
"repocard.issues-commented": {
|
||||
en: "my Commented Issues",
|
||||
},
|
||||
"statcard.prs-merged": {
|
||||
ar: "مجموع الطلبات المدمجة",
|
||||
cn: "合并的 PR 总数",
|
||||
|
||||
Reference in New Issue
Block a user