mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-05-15 14:38:03 +02:00
3410 lines
118 KiB
JavaScript
3410 lines
118 KiB
JavaScript
angular
|
||
.module("anonymous-github", [
|
||
"ngRoute",
|
||
"ngSanitize",
|
||
"ui.ace",
|
||
"ngPDFViewer",
|
||
"pascalprecht.translate",
|
||
"admin",
|
||
])
|
||
.config([
|
||
"$routeProvider",
|
||
"$locationProvider",
|
||
"$translateProvider",
|
||
function ($routeProvider, $locationProvider, $translateProvider) {
|
||
$translateProvider.useStaticFilesLoader({
|
||
prefix: "/i18n/locale-",
|
||
suffix: ".json",
|
||
});
|
||
|
||
$translateProvider.preferredLanguage("en");
|
||
|
||
$routeProvider
|
||
.when("/", {
|
||
templateUrl: "/partials/home.htm",
|
||
controller: "homeController",
|
||
title: "Anonymous GitHub – Share the code, not the author",
|
||
})
|
||
.when("/dashboard", {
|
||
templateUrl: "/partials/dashboard.htm",
|
||
controller: "unifiedDashboardController",
|
||
title: "Your anonymizations – Anonymous GitHub",
|
||
})
|
||
.when("/pr-dashboard", {
|
||
redirectTo: "/dashboard",
|
||
})
|
||
.when("/anonymize/:repoId?", {
|
||
templateUrl: "/partials/anonymize.htm",
|
||
controller: "anonymizeController",
|
||
title: "New anonymization – Anonymous GitHub",
|
||
})
|
||
.when("/pull-request-anonymize/:pullRequestId?", {
|
||
templateUrl: "/partials/anonymize.htm",
|
||
controller: "anonymizeController",
|
||
title: "Anonymize a pull request – Anonymous GitHub",
|
||
})
|
||
.when("/gist-anonymize/:gistId?", {
|
||
templateUrl: "/partials/anonymize.htm",
|
||
controller: "anonymizeController",
|
||
title: "Anonymize a gist – Anonymous GitHub",
|
||
})
|
||
.when("/status/:repoId", {
|
||
templateUrl: "/partials/status.htm",
|
||
controller: "statusController",
|
||
title: "Repository status – Anonymous GitHub",
|
||
})
|
||
.when("/conferences", {
|
||
templateUrl: "/partials/conferences.htm",
|
||
controller: "conferencesController",
|
||
title: "Your conferences – Anonymous GitHub",
|
||
})
|
||
.when("/conference/new", {
|
||
templateUrl: "/partials/newConference.htm",
|
||
controller: "newConferenceController",
|
||
title: "New conference – Anonymous GitHub",
|
||
})
|
||
.when("/conference/:conferenceId/edit", {
|
||
templateUrl: "/partials/newConference.htm",
|
||
controller: "newConferenceController",
|
||
title: "Edit conference – Anonymous GitHub",
|
||
})
|
||
.when("/conference/:conferenceId", {
|
||
templateUrl: "/partials/conference.htm",
|
||
controller: "conferenceController",
|
||
title: "Conference – Anonymous GitHub",
|
||
})
|
||
.when("/faq", {
|
||
templateUrl: "/partials/faq.htm",
|
||
controller: "faqController",
|
||
title: "FAQ – Anonymous GitHub",
|
||
})
|
||
.when("/profile", {
|
||
templateUrl: "/partials/profile.htm",
|
||
controller: "profileController",
|
||
title: "Your settings – Anonymous GitHub",
|
||
})
|
||
.when("/claim", {
|
||
templateUrl: "/partials/claim.htm",
|
||
controller: "claimController",
|
||
title: "Claim an anonymization – Anonymous GitHub",
|
||
})
|
||
.when("/pr/:pullRequestId", {
|
||
templateUrl: "/partials/pullRequest.htm",
|
||
controller: "pullRequestController",
|
||
title: "Anonymous pull request – Anonymous GitHub",
|
||
reloadOnUrl: false,
|
||
})
|
||
.when("/gist/:gistId", {
|
||
templateUrl: "/partials/gist.htm",
|
||
controller: "gistController",
|
||
title: "Anonymous gist – Anonymous GitHub",
|
||
reloadOnUrl: false,
|
||
})
|
||
.when("/r/:repoId/:path*?", {
|
||
templateUrl: "/partials/explorer.htm",
|
||
controller: "exploreController",
|
||
title: "Anonymous repository – Anonymous GitHub",
|
||
reloadOnUrl: false,
|
||
})
|
||
.when("/repository/:repoId/:path*?", {
|
||
templateUrl: "/partials/explorer.htm",
|
||
controller: "exploreController",
|
||
title: "Anonymous repository – Anonymous GitHub",
|
||
reloadOnUrl: false,
|
||
})
|
||
.when("/admin/", {
|
||
templateUrl: "/partials/admin/overview.htm",
|
||
controller: "overviewAdminController",
|
||
title: "Admin · Overview – Anonymous GitHub",
|
||
})
|
||
.when("/admin/repositories", {
|
||
templateUrl: "/partials/admin/repositories.htm",
|
||
controller: "repositoriesAdminController",
|
||
title: "Admin · Repositories – Anonymous GitHub",
|
||
})
|
||
.when("/admin/users", {
|
||
templateUrl: "/partials/admin/users.htm",
|
||
controller: "usersAdminController",
|
||
title: "Admin · Users – Anonymous GitHub",
|
||
})
|
||
.when("/admin/users/:username", {
|
||
templateUrl: "/partials/admin/user.htm",
|
||
controller: "userAdminController",
|
||
title: "Admin · User details – Anonymous GitHub",
|
||
})
|
||
.when("/admin/conferences", {
|
||
templateUrl: "/partials/admin/conferences.htm",
|
||
controller: "conferencesAdminController",
|
||
title: "Admin · Conferences – Anonymous GitHub",
|
||
})
|
||
.when("/admin/queues", {
|
||
templateUrl: "/partials/admin/queues.htm",
|
||
controller: "queuesAdminController",
|
||
title: "Admin · Queues – Anonymous GitHub",
|
||
})
|
||
.when("/admin/errors", {
|
||
templateUrl: "/partials/admin/errors.htm",
|
||
controller: "errorsAdminController",
|
||
title: "Admin · Errors – Anonymous GitHub",
|
||
})
|
||
.when("/404", {
|
||
templateUrl: "/partials/404.htm",
|
||
title: "Page not found – Anonymous GitHub",
|
||
})
|
||
.otherwise({
|
||
templateUrl: "/partials/404.htm",
|
||
title: "Page not found – Anonymous GitHub",
|
||
});
|
||
$locationProvider.html5Mode(true);
|
||
},
|
||
])
|
||
.filter("humanFileSize", function () {
|
||
return humanFileSize;
|
||
})
|
||
.filter("bigNum", function () {
|
||
return function bigNum(v) {
|
||
const n = Number(v) || 0;
|
||
const abs = Math.abs(n);
|
||
if (abs < 1000) return String(n);
|
||
if (abs < 10000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
|
||
if (abs < 1000000) return Math.round(n / 1000) + "k";
|
||
if (abs < 10000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
|
||
return Math.round(n / 1000000) + "M";
|
||
};
|
||
})
|
||
.filter("humanTime", function () {
|
||
return function humanTime(seconds) {
|
||
if (!seconds) {
|
||
return "never";
|
||
}
|
||
if (seconds instanceof Date)
|
||
seconds = Math.round((Date.now() - seconds) / 1000);
|
||
if (typeof seconds == "string" || typeof seconds == "number")
|
||
seconds = Math.round((Date.now() - new Date(seconds)) / 1000);
|
||
var suffix = seconds < 0 ? "from now" : "ago";
|
||
|
||
// more than 2 days ago display Date
|
||
if (Math.abs(seconds) > 2 * 60 * 60 * 24) {
|
||
const now = new Date();
|
||
now.setSeconds(now.getSeconds() - seconds);
|
||
return "on " + now.toLocaleDateString();
|
||
}
|
||
|
||
seconds = Math.abs(seconds);
|
||
|
||
var times = [
|
||
seconds / 60 / 60 / 24 / 365, // years
|
||
seconds / 60 / 60 / 24 / 30, // months
|
||
seconds / 60 / 60 / 24 / 7, // weeks
|
||
seconds / 60 / 60 / 24, // days
|
||
seconds / 60 / 60, // hours
|
||
seconds / 60, // minutes
|
||
seconds, // seconds
|
||
];
|
||
var names = ["year", "month", "week", "day", "hour", "minute", "second"];
|
||
|
||
for (var i = 0; i < names.length; i++) {
|
||
var time = Math.floor(times[i]);
|
||
var name = names[i];
|
||
if (time > 1) name += "s";
|
||
|
||
if (time >= 1) return time + " " + name + " " + suffix;
|
||
}
|
||
return "0 seconds " + suffix;
|
||
};
|
||
})
|
||
.filter("title", function () {
|
||
return function (str) {
|
||
if (!str) return str;
|
||
|
||
str = str.toLowerCase();
|
||
var words = str.split(" ");
|
||
|
||
var capitalized = words.map(function (word) {
|
||
return word.charAt(0).toUpperCase() + word.substring(1, word.length);
|
||
});
|
||
return capitalized.join(" ");
|
||
};
|
||
})
|
||
.filter("statusMsg", function () {
|
||
return function (msg) {
|
||
if (!msg) return msg;
|
||
var m = msg.match(/^rate_limited:(\d+)$/);
|
||
if (m) {
|
||
var remaining = Math.max(0, Math.ceil((parseInt(m[1], 10) - Date.now()) / 1000));
|
||
if (remaining <= 0) return "Rate limited — resuming soon";
|
||
var min = Math.floor(remaining / 60);
|
||
var sec = remaining % 60;
|
||
return "Rate limited — retrying in " + (min > 0 ? min + "m " + sec + "s" : sec + "s");
|
||
}
|
||
return msg;
|
||
};
|
||
})
|
||
.filter("diff", [
|
||
"$sce",
|
||
function ($sce) {
|
||
const esc = (s) =>
|
||
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
|
||
function flushFile(out, file) {
|
||
if (!file) return;
|
||
const headerName =
|
||
file.newPath && file.newPath !== "/dev/null"
|
||
? file.newPath
|
||
: file.oldPath || "";
|
||
const status =
|
||
file.oldPath === "/dev/null"
|
||
? "added"
|
||
: file.newPath === "/dev/null"
|
||
? "deleted"
|
||
: file.oldPath && file.newPath && file.oldPath !== file.newPath
|
||
? "renamed"
|
||
: "modified";
|
||
out.push('<div class="diff-file-block">');
|
||
out.push(
|
||
'<div class="diff-file-header"><span class="diff-file-icon"><i class="far fa-file-code"></i></span>' +
|
||
'<span class="diff-file-name">' +
|
||
esc(headerName) +
|
||
"</span>" +
|
||
'<span class="diff-file-status diff-file-status-' +
|
||
status +
|
||
'">' +
|
||
status +
|
||
"</span></div>"
|
||
);
|
||
if (file.lines.length) {
|
||
out.push('<table class="diff-file-table"><tbody>');
|
||
for (const line of file.lines) {
|
||
out.push(
|
||
'<tr class="diff-row diff-row-' +
|
||
line.kind +
|
||
'">' +
|
||
'<td class="diff-gutter diff-gutter-old">' +
|
||
(line.oldNo || "") +
|
||
"</td>" +
|
||
'<td class="diff-gutter diff-gutter-new">' +
|
||
(line.newNo || "") +
|
||
"</td>" +
|
||
'<td class="diff-sign">' +
|
||
(line.kind === "add"
|
||
? "+"
|
||
: line.kind === "remove"
|
||
? "-"
|
||
: line.kind === "hunk"
|
||
? "@"
|
||
: "") +
|
||
"</td>" +
|
||
'<td class="diff-code">' +
|
||
esc(line.text) +
|
||
"</td>" +
|
||
"</tr>"
|
||
);
|
||
}
|
||
out.push("</tbody></table>");
|
||
}
|
||
out.push("</div>");
|
||
}
|
||
|
||
return function (str) {
|
||
if (!str) return str;
|
||
const out = [];
|
||
let file = null;
|
||
let oldNo = 0;
|
||
let newNo = 0;
|
||
const ensureFile = () => {
|
||
if (!file) file = { oldPath: "", newPath: "", lines: [] };
|
||
return file;
|
||
};
|
||
const startNewFileIfNeeded = () => {
|
||
if (file && (file.lines.length || file.oldPath || file.newPath)) {
|
||
flushFile(out, file);
|
||
file = null;
|
||
}
|
||
};
|
||
const lines = str.split("\n");
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const ln = lines[i];
|
||
if (ln.startsWith("diff --git")) {
|
||
startNewFileIfNeeded();
|
||
ensureFile();
|
||
continue;
|
||
}
|
||
if (ln.startsWith("--- ")) {
|
||
// New file boundary if the previous file already had lines.
|
||
if (file && file.lines.length) startNewFileIfNeeded();
|
||
ensureFile().oldPath = ln.replace(/^--- (a\/)?/, "").trim();
|
||
continue;
|
||
}
|
||
if (ln.startsWith("+++ ")) {
|
||
ensureFile().newPath = ln.replace(/^\+\+\+ (b\/)?/, "").trim();
|
||
continue;
|
||
}
|
||
if (
|
||
ln.startsWith("index ") ||
|
||
ln.startsWith("similarity index") ||
|
||
ln.startsWith("rename ") ||
|
||
ln.startsWith("new file mode") ||
|
||
ln.startsWith("deleted file mode") ||
|
||
ln.startsWith("Binary files")
|
||
) {
|
||
continue;
|
||
}
|
||
if (ln.startsWith("@@")) {
|
||
const m = ln.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
|
||
if (m) {
|
||
oldNo = parseInt(m[1], 10);
|
||
newNo = parseInt(m[2], 10);
|
||
}
|
||
ensureFile().lines.push({ kind: "hunk", oldNo: "", newNo: "", text: ln });
|
||
continue;
|
||
}
|
||
if (!file) continue;
|
||
if (ln.startsWith("+")) {
|
||
file.lines.push({ kind: "add", oldNo: "", newNo: newNo, text: ln.slice(1) });
|
||
newNo++;
|
||
} else if (ln.startsWith("-")) {
|
||
file.lines.push({ kind: "remove", oldNo: oldNo, newNo: "", text: ln.slice(1) });
|
||
oldNo++;
|
||
} else {
|
||
file.lines.push({ kind: "ctx", oldNo: oldNo, newNo: newNo, text: ln.startsWith(" ") ? ln.slice(1) : ln });
|
||
oldNo++;
|
||
newNo++;
|
||
}
|
||
}
|
||
flushFile(out, file);
|
||
return $sce.trustAsHtml(out.join(""));
|
||
};
|
||
},
|
||
])
|
||
.directive("gistFile", [
|
||
"$location",
|
||
"$timeout",
|
||
"$sce",
|
||
function ($location, $timeout, $sce) {
|
||
// Map GitHub `language` and file extensions to Prism aliases. Prism
|
||
// only ships a handful of grammars (js/py/r/julia/markup); unknown
|
||
// classes still render as readable <pre><code>.
|
||
const langAliases = {
|
||
javascript: "javascript",
|
||
js: "javascript",
|
||
typescript: "javascript",
|
||
ts: "javascript",
|
||
jsx: "javascript",
|
||
tsx: "javascript",
|
||
python: "python",
|
||
py: "python",
|
||
ipynb: "json",
|
||
r: "r",
|
||
julia: "julia",
|
||
html: "markup",
|
||
xml: "markup",
|
||
svg: "markup",
|
||
json: "json",
|
||
yaml: "yaml",
|
||
yml: "yaml",
|
||
bash: "bash",
|
||
sh: "bash",
|
||
shell: "bash",
|
||
css: "css",
|
||
scss: "css",
|
||
c: "c",
|
||
"c++": "cpp",
|
||
cpp: "cpp",
|
||
java: "java",
|
||
go: "go",
|
||
rust: "rust",
|
||
ruby: "ruby",
|
||
php: "php",
|
||
sql: "sql",
|
||
diff: "diff",
|
||
};
|
||
function ext(filename) {
|
||
const i = (filename || "").lastIndexOf(".");
|
||
return i < 0 ? "" : filename.slice(i + 1).toLowerCase();
|
||
}
|
||
function langFor(file) {
|
||
const fromLang =
|
||
file && file.language && langAliases[file.language.toLowerCase()];
|
||
if (fromLang) return fromLang;
|
||
const fromExt = langAliases[ext(file && file.filename)];
|
||
return fromExt || "none";
|
||
}
|
||
function kind(file) {
|
||
const e = ext(file && file.filename);
|
||
if (e === "md" || e === "markdown" || (file && file.language === "Markdown"))
|
||
return "md";
|
||
return "code";
|
||
}
|
||
return {
|
||
restrict: "E",
|
||
scope: { file: "=", terms: "=", options: "=" },
|
||
template:
|
||
'<div ng-if="kind === \'md\'"><markdown content="file.content" terms="terms" options="options"></markdown></div>' +
|
||
'<pre ng-if="kind === \'code\'" class="line-numbers"><code class="{{prismClass}}" ng-bind="file.content"></code></pre>',
|
||
link: function (scope, elem) {
|
||
function update() {
|
||
if (!scope.file) return;
|
||
scope.kind = kind(scope.file);
|
||
scope.prismClass = "language-" + langFor(scope.file);
|
||
// Re-run Prism after the new <code> lands in the DOM.
|
||
$timeout(() => {
|
||
const codes = elem[0].querySelectorAll("pre code");
|
||
codes.forEach((c) => {
|
||
if (window.Prism) Prism.highlightElement(c);
|
||
});
|
||
}, 50);
|
||
}
|
||
scope.$watch("file", update);
|
||
scope.$watch("file.content", update);
|
||
scope.$watch("terms", update);
|
||
scope.$watch("options", update, true);
|
||
},
|
||
};
|
||
},
|
||
])
|
||
.directive("markdown", [
|
||
"$location",
|
||
function ($location) {
|
||
return {
|
||
restrict: "E",
|
||
scope: {
|
||
terms: "=",
|
||
options: "=",
|
||
content: "=",
|
||
},
|
||
link: function (scope, elem, attrs) {
|
||
function update() {
|
||
elem.html(renderMD(scope.content, $location.url() + "/../"));
|
||
}
|
||
scope.$watch(attrs.terms, update);
|
||
scope.$watch("terms", update);
|
||
scope.$watch("options", update);
|
||
scope.$watch("content", update);
|
||
},
|
||
};
|
||
},
|
||
])
|
||
.directive("tree", [
|
||
function () {
|
||
return {
|
||
restrict: "E",
|
||
scope: { file: "=", parent: "@", searchQuery: "=", searchResults: "=" },
|
||
controller: [
|
||
"$element",
|
||
"$scope",
|
||
"$routeParams",
|
||
"$compile",
|
||
function ($element, $scope, $routeParams, $compile) {
|
||
$scope.repoId = document.location.pathname.split("/")[2];
|
||
|
||
$scope.opens = {};
|
||
|
||
if ($routeParams.path) {
|
||
let accumulatedPath = "";
|
||
$routeParams.path.split("/").forEach((f) => {
|
||
$scope.opens[accumulatedPath + "/" + f] = true;
|
||
accumulatedPath = accumulatedPath + "/" + f;
|
||
});
|
||
}
|
||
|
||
const toArray = function (arr) {
|
||
const output = [];
|
||
const keys = { "": { child: output } };
|
||
function ensurePath(path) {
|
||
if (keys[path]) return;
|
||
const segments = path.split("/");
|
||
let acc = "";
|
||
for (let i = 0; i < segments.length; i++) {
|
||
const parent = acc;
|
||
acc = acc ? acc + "/" + segments[i] : segments[i];
|
||
if (!keys[acc]) {
|
||
const dir = { name: segments[i], child: [] };
|
||
keys[acc] = dir;
|
||
keys[parent].child.push(dir);
|
||
}
|
||
}
|
||
}
|
||
for (let file of arr) {
|
||
if (file.path && !keys[file.path]) {
|
||
ensurePath(file.path);
|
||
}
|
||
let current = keys[file.path || ""].child;
|
||
let fPath = `${file.path}/${file.name}`;
|
||
if (fPath.startsWith("/")) {
|
||
fPath = fPath.substring(1);
|
||
}
|
||
if (file.size != null) {
|
||
current.push({
|
||
name: file.name,
|
||
size: file.size,
|
||
sha: file.sha,
|
||
});
|
||
} else {
|
||
if (!keys[fPath]) {
|
||
const dir = {
|
||
name: file.name,
|
||
child: [],
|
||
};
|
||
keys[fPath] = dir;
|
||
current.push(dir);
|
||
}
|
||
}
|
||
}
|
||
return output;
|
||
};
|
||
|
||
const sortFiles = (f1, f2) => {
|
||
const f1d = !!f1.child;
|
||
const f2d = !!f2.child;
|
||
if (f1d && f2d) {
|
||
return f1.name.localeCompare(f2.name);
|
||
}
|
||
if (f1d) {
|
||
return -1;
|
||
}
|
||
if (f2d) {
|
||
return 1;
|
||
}
|
||
return f1.name.localeCompare(f2.name);
|
||
};
|
||
|
||
function getFileCount(folderPath) {
|
||
const counts = $scope.$parent.fileCounts;
|
||
if (!counts) return 0;
|
||
const normalized = folderPath.startsWith("/")
|
||
? folderPath.substring(1)
|
||
: folderPath;
|
||
return counts[normalized] || 0;
|
||
}
|
||
|
||
function isTruncated(folderPath) {
|
||
const truncated =
|
||
($scope.$parent.options &&
|
||
$scope.$parent.options.truncatedFolders) ||
|
||
[];
|
||
if (!truncated.length) return false;
|
||
const normalized = folderPath.startsWith("/")
|
||
? folderPath.substring(1)
|
||
: folderPath;
|
||
return truncated.indexOf(normalized) !== -1;
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||
}
|
||
|
||
function buildSearchFilter() {
|
||
const results = $scope.searchResults;
|
||
if (!results || !results.length) return null;
|
||
const matchPaths = new Set();
|
||
const matchFolders = new Set();
|
||
for (const f of results) {
|
||
const full = f.path ? `${f.path}/${f.name}` : f.name;
|
||
matchPaths.add(full);
|
||
// Also collect all ancestor folders
|
||
if (f.path) {
|
||
const segments = f.path.split("/").filter(Boolean);
|
||
let acc = "";
|
||
for (const seg of segments) {
|
||
acc = acc ? `${acc}/${seg}` : seg;
|
||
matchFolders.add(acc);
|
||
}
|
||
}
|
||
}
|
||
return { paths: matchPaths, folders: matchFolders };
|
||
}
|
||
|
||
function nodeMatchesFilter(node, parentPath, filterSet) {
|
||
if (!filterSet) return true;
|
||
const path = parentPath
|
||
? `${parentPath}/${node.name}`
|
||
: node.name;
|
||
if (!node.child) {
|
||
return filterSet.paths.has(path);
|
||
}
|
||
// Show folder if it's an ancestor of a match or contains matches
|
||
if (filterSet.folders.has(path)) return true;
|
||
return node.child.some((c) =>
|
||
nodeMatchesFilter(c, path, filterSet)
|
||
);
|
||
}
|
||
|
||
function generate(current, parentPath, filterSet) {
|
||
if (!current) return "";
|
||
current = current.sort(sortFiles);
|
||
let output = "<ul>";
|
||
for (let f of current) {
|
||
if (filterSet && !nodeMatchesFilter(f, parentPath ? parentPath.substring(1) : "", filterSet)) {
|
||
continue;
|
||
}
|
||
let dir = !!f.child;
|
||
let name = f.name;
|
||
let size = f.size;
|
||
let collapsed = f;
|
||
if (dir) {
|
||
let test = name;
|
||
let inner = f.child;
|
||
while (inner && inner.length == 1) {
|
||
test += "/" + inner[0].name;
|
||
size = inner[0].size;
|
||
inner = inner[0].child;
|
||
}
|
||
name = test;
|
||
collapsed = inner ? { child: inner } : f;
|
||
if (size != null && size >= 0) {
|
||
dir = false;
|
||
}
|
||
}
|
||
const sizeTitle = size != null ? `Size: ${humanFileSize(size || 0)}` : "";
|
||
const path = `${parentPath}/${name}`;
|
||
const fileCount = dir ? getFileCount(path) : 0;
|
||
|
||
const isOpen = filterSet ? ($scope.opens[path] !== false) : $scope.opens[path];
|
||
const cssClasses = ["file"];
|
||
if (dir) {
|
||
cssClasses.push("folder");
|
||
}
|
||
if (isOpen) {
|
||
cssClasses.push("open");
|
||
}
|
||
if ($scope.isActive(path)) {
|
||
cssClasses.push("active");
|
||
}
|
||
const truncated = dir && isTruncated(path);
|
||
if (truncated) {
|
||
cssClasses.push("truncated");
|
||
}
|
||
|
||
output += `<li class="${cssClasses.join(
|
||
" "
|
||
)}" ng-class="{active: isActive('${path}'), open: ${filterSet ? "opens['" + path + "'] !== false" : "opens['" + path + "']"}}" title="${escapeHtml(sizeTitle)}">`;
|
||
if (dir) {
|
||
output += `<a ng-click="openFolder('${path}', $event)"><span class="tree-toggle"></span><span class="tree-icon-folder"></span><span class="tree-name">${escapeHtml(name)}</span>`;
|
||
if (truncated) {
|
||
output += `<span class="truncated-warning" title="{{ 'WARNINGS.folder_truncated' | translate }}"><i class="fas fa-exclamation-triangle"></i></span>`;
|
||
}
|
||
if (fileCount > 0) {
|
||
output += `<span class="tree-count">${fileCount}</span>`;
|
||
}
|
||
output += `</a>`;
|
||
} else {
|
||
const needsSpacer = parentPath !== "";
|
||
output += `<a href='/r/${$scope.repoId}${encodePathForUrl(
|
||
path
|
||
)}'>${needsSpacer ? '<span class="tree-spacer"></span>' : ''}<span class="tree-icon-file"></span><span class="tree-name">${escapeHtml(name)}</span></a>`;
|
||
}
|
||
if (isOpen && collapsed.child) {
|
||
const children = collapsed.child;
|
||
if (children.length > 1) {
|
||
output += generate(children, path, filterSet);
|
||
} else if (dir) {
|
||
let inner = children;
|
||
while (inner && inner.length == 1) {
|
||
inner = inner[0].child;
|
||
}
|
||
output += generate(inner, path, filterSet);
|
||
}
|
||
}
|
||
output += "</li>";
|
||
}
|
||
return output + "</ul>";
|
||
}
|
||
|
||
function display() {
|
||
$element.html("");
|
||
const filterSet = $scope.searchQuery ? buildSearchFilter() : null;
|
||
let output;
|
||
if (filterSet !== null && filterSet.paths.size === 0) {
|
||
output = '<div class="tree-search-empty">No files found</div>';
|
||
} else {
|
||
output = generate(toArray($scope.file).sort(sortFiles), "", filterSet);
|
||
}
|
||
$compile(output)($scope, (clone) => {
|
||
$element.append(clone);
|
||
restoreFocus();
|
||
});
|
||
}
|
||
|
||
function expandAllFolders(nodes, parentPath) {
|
||
if (!nodes) return;
|
||
for (const f of nodes) {
|
||
if (!f.child || f.child.length === 0) continue;
|
||
const path = `${parentPath}/${f.name}`;
|
||
if (!(path in $scope.opens)) {
|
||
$scope.opens[path] = true;
|
||
}
|
||
expandAllFolders(f.child, path);
|
||
}
|
||
}
|
||
|
||
$scope.$watch(
|
||
"file",
|
||
(newValue) => {
|
||
if (newValue == null) return;
|
||
if (newValue.length == 0) {
|
||
return $element.html("Empty repository");
|
||
}
|
||
expandAllFolders(toArray(newValue), "");
|
||
display();
|
||
},
|
||
true
|
||
);
|
||
|
||
$scope.$watch("searchResults", (newVal, oldVal) => {
|
||
if (newVal === oldVal) return;
|
||
if ($scope.file && $scope.file.length) {
|
||
display();
|
||
}
|
||
});
|
||
|
||
$scope.$watch("searchQuery", (newVal, oldVal) => {
|
||
if (newVal === oldVal) return;
|
||
if (!newVal && $scope.file && $scope.file.length) {
|
||
display();
|
||
}
|
||
});
|
||
|
||
$scope.isActive = function (name) {
|
||
return $routeParams.path == name.substring(1);
|
||
};
|
||
|
||
$scope.openFolder = function (folder, event) {
|
||
var currentlyOpen = $scope.opens[folder];
|
||
if (currentlyOpen === undefined && $scope.searchQuery) {
|
||
currentlyOpen = true;
|
||
}
|
||
$scope.opens[folder] = !currentlyOpen;
|
||
const li = event.target.closest("li");
|
||
const childUl = li ? li.querySelector(":scope > ul") : null;
|
||
const needsLoad =
|
||
childUl == null ||
|
||
childUl.children.length === 0;
|
||
if (needsLoad) {
|
||
$scope.$parent.getFiles(folder.substring(1));
|
||
}
|
||
};
|
||
|
||
var focusedPath = $routeParams.path ? "/" + $routeParams.path : null;
|
||
|
||
function getVisibleLinks() {
|
||
return Array.from($element[0].querySelectorAll("li > a"));
|
||
}
|
||
|
||
function getFocusedLink() {
|
||
return $element[0].querySelector("a.tree-focused");
|
||
}
|
||
|
||
function getLinkPath(link) {
|
||
if (!link) return null;
|
||
var href = link.getAttribute("href");
|
||
if (href) {
|
||
var prefix = "/r/" + $scope.repoId;
|
||
return href.indexOf(prefix) === 0 ? decodeURIComponent(href.substring(prefix.length)) : null;
|
||
}
|
||
var onclick = link.getAttribute("ng-click");
|
||
if (onclick) {
|
||
var m = onclick.match(/openFolder\('([^']+)'/);
|
||
return m ? m[1] : null;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function findLinkByPath(path) {
|
||
if (!path) return null;
|
||
var links = getVisibleLinks();
|
||
for (var i = 0; i < links.length; i++) {
|
||
if (getLinkPath(links[i]) === path) return links[i];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function setFocus(link) {
|
||
var prev = getFocusedLink();
|
||
if (prev) prev.classList.remove("tree-focused");
|
||
if (link) {
|
||
link.classList.add("tree-focused");
|
||
link.scrollIntoView({ block: "nearest" });
|
||
focusedPath = getLinkPath(link);
|
||
} else {
|
||
focusedPath = null;
|
||
}
|
||
}
|
||
|
||
function restoreFocus() {
|
||
if (!focusedPath) return;
|
||
var link = findLinkByPath(focusedPath);
|
||
if (link) {
|
||
link.classList.add("tree-focused");
|
||
$element[0].focus();
|
||
}
|
||
}
|
||
|
||
$element[0].setAttribute("tabindex", "0");
|
||
|
||
$element[0].addEventListener("keydown", function (e) {
|
||
var links = getVisibleLinks();
|
||
if (!links.length) return;
|
||
var focused = getFocusedLink();
|
||
var idx = focused ? links.indexOf(focused) : -1;
|
||
|
||
if (e.key === "ArrowDown") {
|
||
e.preventDefault();
|
||
var next = idx < links.length - 1 ? idx + 1 : 0;
|
||
setFocus(links[next]);
|
||
} else if (e.key === "ArrowUp") {
|
||
e.preventDefault();
|
||
var prev = idx > 0 ? idx - 1 : links.length - 1;
|
||
setFocus(links[prev]);
|
||
} else if (e.key === "ArrowRight") {
|
||
e.preventDefault();
|
||
if (!focused) return;
|
||
var li = focused.closest("li");
|
||
if (li && li.classList.contains("folder")) {
|
||
if (!li.classList.contains("open")) {
|
||
focused.click();
|
||
} else {
|
||
var childLink = li.querySelector(":scope > ul > li > a");
|
||
if (childLink) setFocus(childLink);
|
||
}
|
||
}
|
||
} else if (e.key === "ArrowLeft") {
|
||
e.preventDefault();
|
||
if (!focused) return;
|
||
var li = focused.closest("li");
|
||
if (li && li.classList.contains("folder") && li.classList.contains("open")) {
|
||
focused.click();
|
||
} else {
|
||
var parentLi = li && li.parentElement ? li.parentElement.closest("li.folder") : null;
|
||
if (parentLi) {
|
||
var parentLink = parentLi.querySelector(":scope > a");
|
||
if (parentLink) setFocus(parentLink);
|
||
}
|
||
}
|
||
} else if (e.key === "Enter") {
|
||
e.preventDefault();
|
||
if (focused) focused.click();
|
||
}
|
||
});
|
||
|
||
$element[0].addEventListener("click", function (e) {
|
||
var link = e.target.closest("a");
|
||
if (link && $element[0].contains(link)) {
|
||
setFocus(link);
|
||
}
|
||
});
|
||
},
|
||
],
|
||
};
|
||
},
|
||
])
|
||
.directive("notebook", [
|
||
function () {
|
||
return {
|
||
restrict: "E",
|
||
scope: { file: "=" },
|
||
controller: [
|
||
"$element",
|
||
"$scope",
|
||
"$http",
|
||
function ($element, $scope, $http) {
|
||
function renderNotebookJSON(json) {
|
||
const notebook = nb.parse(json);
|
||
try {
|
||
$element.html("");
|
||
$element.append(notebook.render());
|
||
Prism.highlightAll();
|
||
} catch (error) {
|
||
$element.html("Unable to render the notebook.");
|
||
}
|
||
}
|
||
function render() {
|
||
if ($scope.$parent.content) {
|
||
try {
|
||
renderNotebookJSON(JSON.parse($scope.$parent.content));
|
||
} catch (error) {
|
||
$element.html(
|
||
"Unable to render the notebook invalid notebook format."
|
||
);
|
||
}
|
||
} else if ($scope.file) {
|
||
$http
|
||
.get($scope.file.download_url)
|
||
.then((res) => renderNotebookJSON(res.data));
|
||
}
|
||
}
|
||
$scope.$watch("file", (v) => {
|
||
render();
|
||
});
|
||
render();
|
||
},
|
||
],
|
||
};
|
||
},
|
||
])
|
||
.directive("loc", [
|
||
function () {
|
||
return {
|
||
restrict: "E",
|
||
scope: { stats: "=" },
|
||
template:
|
||
"<div class='lang' ng-repeat='lang in elements' title='{{lang.lang|title}}: {{lang.loc | number}} lines' data-toggle='tooltip' data-placement='bottom' style='width:{{lang.loc*100/total}}%;background:{{lang.color}};'></div>",
|
||
controller: [
|
||
"$scope",
|
||
function ($scope) {
|
||
function render() {
|
||
$scope.elements = [];
|
||
$scope.total = 0;
|
||
for (let lang in $scope.stats) {
|
||
const loc = $scope.stats[lang].code;
|
||
if (!loc) {
|
||
continue;
|
||
}
|
||
$scope.total += loc;
|
||
$scope.elements.push({
|
||
lang,
|
||
loc,
|
||
color: langColors[lang],
|
||
});
|
||
}
|
||
setTimeout(() => {
|
||
$('[data-toggle="tooltip"]').tooltip();
|
||
}, 100);
|
||
}
|
||
|
||
$scope.$watch("stats", (v) => {
|
||
render();
|
||
});
|
||
render();
|
||
},
|
||
],
|
||
};
|
||
},
|
||
])
|
||
.controller("mainController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
"$timeout",
|
||
function ($scope, $http, $location, $timeout) {
|
||
$scope.title = "Main";
|
||
$scope.user = { status: "connection" };
|
||
$scope.site_options;
|
||
|
||
$scope.toasts = [];
|
||
|
||
$scope.removeToast = function (toast) {
|
||
const index = $scope.toasts.indexOf(toast);
|
||
if (index === -1) return;
|
||
$scope.toasts.splice(index, 1);
|
||
};
|
||
|
||
// Auto-dismiss toasts after a fixed delay so they don't pile up across
|
||
// navigations (e.g. the "README not found" toast re-fired every time the
|
||
// edit screen was reopened — see #246). Long-running operations that
|
||
// mutate the toast (remove/refresh) will simply disappear once the
|
||
// delay elapses; users can re-check status from the dashboard.
|
||
$scope.addToast = function (toast) {
|
||
$scope.toasts.push(toast);
|
||
$timeout(function () {
|
||
$scope.removeToast(toast);
|
||
}, 8000);
|
||
return toast;
|
||
};
|
||
|
||
$scope.path = $location.url();
|
||
$scope.paths = $location.path().substring(1).split("/");
|
||
|
||
$scope.darkMode = function (on) {
|
||
localStorage.setItem("darkMode", on);
|
||
$scope.isDarkMode = on;
|
||
const darkPrismLink = "/css/prism-okaidia.css";
|
||
const lightPrismLink = "/css/prism.css";
|
||
if (on) {
|
||
$("body").addClass("dark-mode");
|
||
let link = document.createElement("link");
|
||
link.href = darkPrismLink;
|
||
link.rel = "stylesheet";
|
||
document.head.append(link);
|
||
$(`link[href='${lightPrismLink}']`).remove();
|
||
} else {
|
||
$("body").removeClass("dark-mode");
|
||
let link = document.createElement("link");
|
||
link.href = lightPrismLink;
|
||
link.rel = "stylesheet";
|
||
document.head.append(link);
|
||
$(`link[href='${darkPrismLink}']`).remove();
|
||
}
|
||
// Update Ko-fi floating button to match theme
|
||
var kofiBtn = document.querySelector(".floatingchat-container-wrap-mo498 .floating-chat-kofi-text-container-wrap");
|
||
if (kofiBtn) {
|
||
kofiBtn.style.backgroundColor = on ? "#FAF9F6" : "#1A1815";
|
||
kofiBtn.style.color = on ? "#1A1815" : "#FAF9F6";
|
||
}
|
||
$scope.$broadcast("dark-mode", on);
|
||
};
|
||
|
||
$scope.darkMode(localStorage.getItem("darkMode") == "true");
|
||
|
||
function getUser() {
|
||
$http.get("/api/user").then(
|
||
(res) => {
|
||
if (res) $scope.user = res.data;
|
||
},
|
||
() => {
|
||
$scope.user = null;
|
||
}
|
||
);
|
||
}
|
||
getUser();
|
||
|
||
function getOptions() {
|
||
$http.get("/api/options").then(
|
||
(res) => {
|
||
if (res) $scope.site_options = res.data;
|
||
},
|
||
() => {
|
||
$scope.site_options = null;
|
||
}
|
||
);
|
||
}
|
||
getOptions();
|
||
|
||
function getMessage() {
|
||
$http.get("/api/message").then(
|
||
(res) => {
|
||
if (res) $scope.generalMessage = res.data;
|
||
},
|
||
() => {
|
||
$scope.generalMessage = null;
|
||
}
|
||
);
|
||
}
|
||
getMessage();
|
||
|
||
function changedUrl(_, current) {
|
||
if (current) {
|
||
$scope.title = current.title;
|
||
}
|
||
$scope.path = $location.url();
|
||
$scope.paths = $location.path().substring(1).split("/");
|
||
}
|
||
|
||
$scope.$on("$routeChangeSuccess", changedUrl);
|
||
$scope.$on("$routeUpdate", changedUrl);
|
||
},
|
||
])
|
||
.controller("faqController", ["$scope", "$http", function ($scope, $http) {}])
|
||
.controller("profileController", [
|
||
"$scope",
|
||
"$http",
|
||
function ($scope, $http) {
|
||
$scope.terms = "";
|
||
$scope.options = {
|
||
expirationMode: "remove",
|
||
update: false,
|
||
image: true,
|
||
pdf: true,
|
||
notebook: true,
|
||
loc: true,
|
||
link: true,
|
||
};
|
||
|
||
function getDefault() {
|
||
$http.get("/api/user/default").then((res) => {
|
||
const data = res.data;
|
||
if (data.terms) {
|
||
$scope.terms = data.terms.join("\n");
|
||
}
|
||
$scope.option = Object.assign({}, $scope.option, data.options);
|
||
});
|
||
}
|
||
getDefault();
|
||
|
||
$scope.saveDefault = () => {
|
||
const params = {
|
||
terms: $scope.terms.trim().split("\n"),
|
||
options: $scope.options,
|
||
};
|
||
$http.post("/api/user/default", params).then(
|
||
() => {
|
||
getDefault();
|
||
$scope.message = "Saved";
|
||
},
|
||
(error) => {
|
||
$translate("ERRORS." + error.data.error).then((translation) => {
|
||
$scope.error = translation;
|
||
}, console.error);
|
||
}
|
||
);
|
||
};
|
||
},
|
||
])
|
||
.controller("claimController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
function ($scope, $http, $location) {
|
||
$scope.repoId = null;
|
||
$scope.repoUrl = null;
|
||
$scope.claim = () => {
|
||
$http
|
||
.post("/api/repo/claim", {
|
||
repoId: $scope.repoId,
|
||
repoUrl: $scope.repoUrl,
|
||
})
|
||
.then(
|
||
(res) => {
|
||
$location.url("/dashboard");
|
||
},
|
||
(err) => {
|
||
$scope.error = err.data;
|
||
$scope.claimForm.repoUrl.$setValidity("not_found", false);
|
||
$scope.claimForm.repoId.$setValidity("not_found", false);
|
||
}
|
||
);
|
||
};
|
||
},
|
||
])
|
||
.controller("homeController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
function ($scope, $http, $location) {
|
||
if ($scope.user && !$scope.user.status) {
|
||
$location.url("/dashboard");
|
||
}
|
||
$scope.$watch("user.status", () => {
|
||
if ($scope.user && !$scope.user.status) {
|
||
$location.url("/dashboard");
|
||
}
|
||
});
|
||
|
||
$scope.cards = [
|
||
{ key: "repositories", total: 0, label: "repositories anonymized" },
|
||
{ key: "users", total: 0, label: "researchers" },
|
||
{ key: "pageViews", total: 0, label: "page views" },
|
||
{ key: "pullRequests", total: 0, label: "pull requests" },
|
||
];
|
||
function getStat() {
|
||
$http.get("/api/stat/").then((res) => {
|
||
$scope.stat = res.data;
|
||
$scope.cards[0].total = res.data.nbRepositories;
|
||
$scope.cards[1].total = res.data.nbUsers;
|
||
$scope.cards[2].total = res.data.nbPageViews;
|
||
$scope.cards[3].total = res.data.nbPullRequests;
|
||
});
|
||
}
|
||
getStat();
|
||
|
||
function buildSeriesView(series) {
|
||
const view = {
|
||
series: series,
|
||
bars: [],
|
||
viewW: 100,
|
||
deltaToday: 0,
|
||
pctChange: 0,
|
||
pctAbs: 0,
|
||
isUp: true,
|
||
};
|
||
if (!series || series.length < 2) return view;
|
||
// Bars represent the *daily increment* (today - yesterday), not the
|
||
// cumulative total. The big number above the chart shows the total.
|
||
const deltas = new Array(series.length - 1);
|
||
for (let i = 1; i < series.length; i++) {
|
||
deltas[i - 1] = series[i] - series[i - 1];
|
||
}
|
||
const n = deltas.length;
|
||
const max = Math.max.apply(null, deltas);
|
||
const min = Math.min.apply(null, deltas);
|
||
// Anchor scale to zero so visually small days look small even when all
|
||
// deltas are positive; only fall back to min when there are negatives.
|
||
const base = Math.min(0, min);
|
||
const range = max - base || 1;
|
||
view.viewW = n * 2;
|
||
view.bars = new Array(n);
|
||
for (let i = 0; i < n; i++) {
|
||
const norm = (deltas[i] - base) / range;
|
||
const h = Math.max(1.5, norm * 34);
|
||
view.bars[i] = {
|
||
x: (i * 2 + 0.25).toFixed(2),
|
||
y: (36 - h).toFixed(2),
|
||
w: "1.5",
|
||
h: h.toFixed(2),
|
||
};
|
||
}
|
||
view.deltaToday = deltas[n - 1];
|
||
if (n >= 2) {
|
||
const prior = deltas[n - 2];
|
||
if (prior) {
|
||
view.pctChange = ((view.deltaToday - prior) / prior) * 100;
|
||
}
|
||
}
|
||
view.pctAbs = Math.round(Math.abs(view.pctChange));
|
||
view.isUp = view.pctChange >= 0;
|
||
return view;
|
||
}
|
||
|
||
$scope.history = {
|
||
repositories: buildSeriesView([]),
|
||
users: buildSeriesView([]),
|
||
pageViews: buildSeriesView([]),
|
||
pullRequests: buildSeriesView([]),
|
||
};
|
||
$http.get("/api/stat/history?days=60").then((res) => {
|
||
const rows = res.data || [];
|
||
$scope.history = {
|
||
repositories: buildSeriesView(rows.map((r) => r.nbRepositories || 0)),
|
||
users: buildSeriesView(rows.map((r) => r.nbUsers || 0)),
|
||
pageViews: buildSeriesView(rows.map((r) => r.nbPageViews || 0)),
|
||
pullRequests: buildSeriesView(rows.map((r) => r.nbPullRequests || 0)),
|
||
};
|
||
});
|
||
},
|
||
])
|
||
.controller("unifiedDashboardController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
function ($scope, $http, $location) {
|
||
$scope.$on("$routeChangeStart", function () {
|
||
$('[data-toggle="tooltip"]').tooltip("dispose");
|
||
});
|
||
$scope.$watch("user.status", () => {
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
});
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
|
||
setTimeout(() => {
|
||
$('[data-toggle="tooltip"]').tooltip();
|
||
}, 250);
|
||
|
||
$scope.items = [];
|
||
$scope.search = "";
|
||
|
||
const dashboardPrefsKey = "dashboard.filterPrefs";
|
||
const dashboardPrefDefaults = {
|
||
typeFilter: "all",
|
||
filters: { status: { ready: true, expired: true, removed: false } },
|
||
orderBy: "-anonymizeDate",
|
||
};
|
||
const savedDashboardPrefs = loadFilterPrefs(dashboardPrefsKey) || {};
|
||
$scope.typeFilter = savedDashboardPrefs.typeFilter || dashboardPrefDefaults.typeFilter;
|
||
$scope.filters = {
|
||
status: Object.assign(
|
||
{},
|
||
dashboardPrefDefaults.filters.status,
|
||
(savedDashboardPrefs.filters && savedDashboardPrefs.filters.status) || {}
|
||
),
|
||
};
|
||
$scope.orderBy = savedDashboardPrefs.orderBy || dashboardPrefDefaults.orderBy;
|
||
|
||
$scope.$watchGroup(
|
||
["typeFilter", "orderBy"],
|
||
() => {
|
||
saveFilterPrefs(dashboardPrefsKey, {
|
||
typeFilter: $scope.typeFilter,
|
||
filters: $scope.filters,
|
||
orderBy: $scope.orderBy,
|
||
});
|
||
}
|
||
);
|
||
$scope.$watch(
|
||
"filters",
|
||
() => {
|
||
saveFilterPrefs(dashboardPrefsKey, {
|
||
typeFilter: $scope.typeFilter,
|
||
filters: $scope.filters,
|
||
orderBy: $scope.orderBy,
|
||
});
|
||
},
|
||
true
|
||
);
|
||
|
||
function getQuota() {
|
||
$http.get("/api/user/quota").then((res) => {
|
||
$scope.quota = res.data;
|
||
$scope.quota.storage.percent = $scope.quota.storage.total
|
||
? ($scope.quota.storage.used * 100) / $scope.quota.storage.total
|
||
: 100;
|
||
$scope.quota.file.percent = $scope.quota.file.total
|
||
? ($scope.quota.file.used * 100) / $scope.quota.file.total
|
||
: 100;
|
||
$scope.quota.repository.percent = $scope.quota.repository.total
|
||
? ($scope.quota.repository.used * 100) /
|
||
$scope.quota.repository.total
|
||
: 100;
|
||
}, console.error);
|
||
}
|
||
getQuota();
|
||
|
||
let loadedRepos = null;
|
||
let loadedPRs = null;
|
||
let loadedGists = null;
|
||
|
||
function mergeItems() {
|
||
$scope.items = (loadedRepos || [])
|
||
.concat(loadedPRs || [])
|
||
.concat(loadedGists || []);
|
||
}
|
||
|
||
function loadAll() {
|
||
loadedRepos = null;
|
||
loadedPRs = null;
|
||
loadedGists = null;
|
||
$http.get("/api/user/anonymized_repositories").then(
|
||
(res) => {
|
||
loadedRepos = res.data.map((repo) => {
|
||
if (!repo.pageView) repo.pageView = 0;
|
||
if (!repo.lastView) repo.lastView = "";
|
||
repo.options.terms = repo.options.terms.filter((f) => f);
|
||
repo._type = "repo";
|
||
repo._id = repo.repoId;
|
||
repo._name = repo.repoId;
|
||
repo._source = repo.source.fullName;
|
||
repo._editUrl = "/anonymize/" + repo.repoId;
|
||
repo._viewUrl = "/r/" + repo.repoId + "/";
|
||
return repo;
|
||
});
|
||
mergeItems();
|
||
},
|
||
(err) => { console.error(err); }
|
||
);
|
||
$http.get("/api/user/anonymized_pull_requests").then(
|
||
(res2) => {
|
||
loadedPRs = res2.data.map((pr) => {
|
||
if (!pr.pageView) pr.pageView = 0;
|
||
if (!pr.lastView) pr.lastView = "";
|
||
pr.options.terms = pr.options.terms.filter((f) => f);
|
||
pr._type = "pr";
|
||
pr._id = pr.pullRequestId;
|
||
pr._name = pr.pullRequestId;
|
||
pr._source = pr.source.repositoryFullName + "#" + pr.source.pullRequestId;
|
||
pr._editUrl = "/pull-request-anonymize/" + pr.pullRequestId;
|
||
pr._viewUrl = "/pr/" + pr.pullRequestId + "/";
|
||
return pr;
|
||
});
|
||
mergeItems();
|
||
},
|
||
(err) => { console.error(err); }
|
||
);
|
||
$http.get("/api/user/anonymized_gists").then(
|
||
(res3) => {
|
||
loadedGists = res3.data.map((g) => {
|
||
if (!g.pageView) g.pageView = 0;
|
||
if (!g.lastView) g.lastView = "";
|
||
g.options.terms = (g.options.terms || []).filter((f) => f);
|
||
g._type = "gist";
|
||
g._id = g.gistId;
|
||
g._name = g.gistId;
|
||
g._source = g.source.gistId;
|
||
g._editUrl = "/gist-anonymize/" + g.gistId;
|
||
g._viewUrl = "/gist/" + g.gistId + "/";
|
||
return g;
|
||
});
|
||
mergeItems();
|
||
},
|
||
(err) => { console.error(err); }
|
||
);
|
||
}
|
||
loadAll();
|
||
|
||
function waitRepoToBeReady(repoId, callback) {
|
||
$http.get("/api/repo/" + repoId).then((res) => {
|
||
for (const item of $scope.items) {
|
||
if (item._type === "repo" && item.repoId == repoId) {
|
||
item.status = res.data.status;
|
||
break;
|
||
}
|
||
}
|
||
if (
|
||
res.data.status == "ready" ||
|
||
res.data.status == "error" ||
|
||
res.data.status == "removed" ||
|
||
res.data.status == "expired"
|
||
) {
|
||
callback(res.data);
|
||
return;
|
||
}
|
||
setTimeout(() => waitRepoToBeReady(repoId, callback), 2500);
|
||
});
|
||
}
|
||
|
||
const labelOf = (t) =>
|
||
t === "repo" ? "repository" : t === "gist" ? "gist" : "pull request";
|
||
const apiBaseOf = (t) =>
|
||
t === "repo" ? "/api/repo" : t === "gist" ? "/api/gist" : "/api/pr";
|
||
|
||
$scope.removeItem = (item) => {
|
||
const label = labelOf(item._type);
|
||
if (confirm(`Are you sure that you want to remove the ${label} ${item._id}?`)) {
|
||
const toast = {
|
||
title: `Removing ${item._id}...`,
|
||
date: new Date(),
|
||
body: `The ${label} ${item._id} is going to be removed.`,
|
||
};
|
||
$scope.addToast(toast);
|
||
const endpoint = `${apiBaseOf(item._type)}/${item._id}`;
|
||
$http.delete(endpoint).then(
|
||
() => {
|
||
if (item._type === "repo") {
|
||
waitRepoToBeReady(item._id, () => {
|
||
toast.title = `${item._id} is removed.`;
|
||
toast.body = `The ${label} ${item._id} is removed.`;
|
||
$scope.$apply();
|
||
});
|
||
} else {
|
||
toast.title = `${item._id} is removed.`;
|
||
toast.body = `The ${label} ${item._id} is removed.`;
|
||
loadAll();
|
||
}
|
||
},
|
||
(error) => {
|
||
toast.title = `Error during the removal of ${item._id}.`;
|
||
toast.body = error.body;
|
||
loadAll();
|
||
}
|
||
);
|
||
}
|
||
};
|
||
|
||
$scope.refreshItem = (item) => {
|
||
const label = labelOf(item._type);
|
||
const toast = {
|
||
title: `Refreshing ${item._id}...`,
|
||
date: new Date(),
|
||
body: `The ${label} ${item._id} is going to be refreshed.`,
|
||
};
|
||
$scope.addToast(toast);
|
||
const endpoint = `${apiBaseOf(item._type)}/${item._id}/refresh`;
|
||
$http.post(endpoint).then(
|
||
() => {
|
||
if (item._type === "repo") {
|
||
waitRepoToBeReady(item._id, () => {
|
||
toast.title = `${item._id} is refreshed.`;
|
||
toast.body = `The ${label} ${item._id} is refreshed.`;
|
||
$scope.$apply();
|
||
});
|
||
} else {
|
||
toast.title = `${item._id} is refreshed.`;
|
||
toast.body = `The ${label} ${item._id} is refreshed.`;
|
||
loadAll();
|
||
}
|
||
},
|
||
(error) => {
|
||
toast.title = `Error during the refresh of ${item._id}.`;
|
||
toast.body = error.body;
|
||
loadAll();
|
||
}
|
||
);
|
||
};
|
||
|
||
$scope.itemFilter = (item) => {
|
||
if ($scope.typeFilter !== "all" && item._type !== $scope.typeFilter) return false;
|
||
if ($scope.filters.status[item.status] == false) return false;
|
||
if ($scope.search.trim().length == 0) return true;
|
||
if (item._source && item._source.indexOf($scope.search) > -1) return true;
|
||
if (item._id.indexOf($scope.search) > -1) return true;
|
||
return false;
|
||
};
|
||
},
|
||
])
|
||
.controller("dashboardController", [
|
||
"$scope",
|
||
"$location",
|
||
function ($scope, $location) {
|
||
$location.url("/dashboard");
|
||
},
|
||
])
|
||
.controller("prDashboardController", [
|
||
"$scope",
|
||
"$location",
|
||
function ($scope, $location) {
|
||
$location.url("/dashboard");
|
||
},
|
||
])
|
||
.controller("statusController", [
|
||
"$scope",
|
||
"$http",
|
||
"$routeParams",
|
||
function ($scope, $http, $routeParams) {
|
||
$scope.repoId = $routeParams.repoId;
|
||
$scope.repo = null;
|
||
$scope.progress = 0;
|
||
$scope.rateLimitResetAt = 0;
|
||
$scope.rateLimitCountdown = "";
|
||
|
||
var countdownTimer = null;
|
||
function startRateLimitCountdown(resetAt) {
|
||
$scope.rateLimitResetAt = resetAt;
|
||
if (countdownTimer) clearInterval(countdownTimer);
|
||
function tick() {
|
||
var remaining = Math.max(0, Math.ceil((resetAt - Date.now()) / 1000));
|
||
if (remaining <= 0) {
|
||
$scope.rateLimitCountdown = "";
|
||
$scope.rateLimitResetAt = 0;
|
||
clearInterval(countdownTimer);
|
||
countdownTimer = null;
|
||
} else {
|
||
var min = Math.floor(remaining / 60);
|
||
var sec = remaining % 60;
|
||
$scope.rateLimitCountdown = min > 0
|
||
? min + "m " + sec + "s"
|
||
: sec + "s";
|
||
}
|
||
$scope.$applyAsync();
|
||
}
|
||
tick();
|
||
countdownTimer = setInterval(tick, 1000);
|
||
}
|
||
$scope.$on("$destroy", function () {
|
||
if (countdownTimer) clearInterval(countdownTimer);
|
||
});
|
||
|
||
function parseStatusMessage(msg) {
|
||
if (!msg) return msg;
|
||
var m = msg.match(/^rate_limited:(\d+)$/);
|
||
if (m) {
|
||
startRateLimitCountdown(parseInt(m[1], 10));
|
||
return null;
|
||
}
|
||
$scope.rateLimitResetAt = 0;
|
||
return msg;
|
||
}
|
||
|
||
$scope.getStatus = () => {
|
||
$http
|
||
.get("/api/repo/" + $scope.repoId, {
|
||
repoId: $scope.repoId,
|
||
repoUrl: $scope.repoUrl,
|
||
})
|
||
.then(
|
||
(res) => {
|
||
$scope.repo = res.data;
|
||
if (res.data.rateLimitResetAt) {
|
||
startRateLimitCountdown(res.data.rateLimitResetAt);
|
||
} else {
|
||
$scope.repo.statusMessage = parseStatusMessage($scope.repo.statusMessage);
|
||
}
|
||
if ($scope.repo.status == "ready") {
|
||
$scope.progress = 100;
|
||
} else if ($scope.repo.status == "queue") {
|
||
$scope.progress = 10;
|
||
} else if ($scope.repo.status == "downloaded") {
|
||
$scope.progress = 50;
|
||
} else if ($scope.repo.status == "download") {
|
||
$scope.progress = 25;
|
||
} else if ($scope.repo.status == "preparing") {
|
||
$scope.progress = 25;
|
||
} else if ($scope.repo.status == "anonymizing") {
|
||
$scope.progress = 75;
|
||
}
|
||
var shouldPoll = $scope.repo.status != "ready";
|
||
if ($scope.repo.status == "error" && !$scope.rateLimitResetAt) {
|
||
shouldPoll = false;
|
||
}
|
||
if (shouldPoll) {
|
||
setTimeout($scope.getStatus, 2000);
|
||
}
|
||
},
|
||
(err) => {
|
||
$scope.error = err.data.error;
|
||
}
|
||
);
|
||
};
|
||
$scope.getStatus();
|
||
},
|
||
])
|
||
.controller("anonymizeController", [
|
||
"$scope",
|
||
"$http",
|
||
"$sce",
|
||
"$routeParams",
|
||
"$location",
|
||
"$translate",
|
||
"$timeout",
|
||
function ($scope, $http, $sce, $routeParams, $location, $translate, $timeout) {
|
||
// Unified state
|
||
$scope.sourceUrl = "";
|
||
$scope.detectedType = null; // 'repo' | 'pr' | 'gist'
|
||
$scope.repoId = "";
|
||
$scope.pullRequestId = "";
|
||
$scope.gistId = "";
|
||
$scope.terms = "";
|
||
$scope.defaultTerms = "";
|
||
$scope.branches = [];
|
||
$scope.source = { branch: "", commit: "" };
|
||
$scope.options = {
|
||
expirationMode: "remove",
|
||
expirationDate: new Date(),
|
||
update: false,
|
||
image: true,
|
||
pdf: true,
|
||
notebook: true,
|
||
link: true,
|
||
body: true,
|
||
title: true,
|
||
origin: false,
|
||
diff: true,
|
||
content: true,
|
||
comments: true,
|
||
username: true,
|
||
date: true,
|
||
};
|
||
$scope.options.expirationDate.setDate(
|
||
$scope.options.expirationDate.getDate() + 90
|
||
);
|
||
$scope.anonymize_readme = "";
|
||
$scope.readme = "";
|
||
$scope.html_readme = "";
|
||
$scope.isUpdate = false;
|
||
|
||
function getDefault(cb) {
|
||
$http.get("/api/user/default").then((res) => {
|
||
const data = res.data;
|
||
if (data.terms) {
|
||
$scope.defaultTerms = data.terms.join("\n");
|
||
}
|
||
$scope.options = Object.assign({}, $scope.options, data.options);
|
||
$scope.options.expirationDate = new Date(
|
||
$scope.options.expirationDate
|
||
);
|
||
$scope.options.expirationDate.setDate(
|
||
$scope.options.expirationDate.getDate() + 90
|
||
);
|
||
if (cb) cb();
|
||
});
|
||
}
|
||
|
||
// Helper to safely set validity on form fields
|
||
function setValidity(field, key, value) {
|
||
if ($scope.anonymize && $scope.anonymize[field]) {
|
||
$scope.anonymize[field].$setValidity(key, value);
|
||
}
|
||
}
|
||
|
||
getDefault(() => {
|
||
// Edit mode: repo
|
||
if ($routeParams.repoId && $routeParams.repoId != "") {
|
||
$scope.isUpdate = true;
|
||
$scope.detectedType = "repo";
|
||
$scope.repoId = $routeParams.repoId;
|
||
$http.get("/api/repo/" + $scope.repoId).then(
|
||
async (res) => {
|
||
$scope.sourceUrl = "https://github.com/" + res.data.source.fullName;
|
||
$scope.terms = res.data.options.terms.filter((f) => f).join("\n");
|
||
$scope.source = res.data.source;
|
||
$scope.role = res.data.role || "owner";
|
||
$scope.coauthors = res.data.coauthors || [];
|
||
// Remember the saved branch so the source.branch watcher knows
|
||
// not to bump source.commit to GitHub HEAD on edit-page load
|
||
// (#360). Without this, just opening the Edit form silently
|
||
// pulled in any new commits and saving — even to toggle a
|
||
// checkbox — picked them up.
|
||
$scope._originalBranch = res.data.source.branch;
|
||
$scope.options = Object.assign({}, $scope.options, res.data.options);
|
||
$scope.conference = res.data.conference;
|
||
$scope.repositoryID = res.data.source.repositoryID;
|
||
if (res.data.options.expirationDate) {
|
||
$scope.options.expirationDate = new Date(res.data.options.expirationDate);
|
||
}
|
||
await Promise.all([getRepoDetails(), getReadme()]);
|
||
anonymizeReadme();
|
||
$scope.$apply();
|
||
},
|
||
() => { $location.url("/404"); }
|
||
);
|
||
$scope.$watch("anonymize", () => {
|
||
if ($scope.anonymize.repoId) $scope.anonymize.repoId.$$element[0].disabled = true;
|
||
if ($scope.anonymize.sourceUrl) $scope.anonymize.sourceUrl.$$element[0].disabled = true;
|
||
});
|
||
}
|
||
// Edit mode: PR
|
||
if ($routeParams.pullRequestId && $routeParams.pullRequestId != "") {
|
||
$scope.isUpdate = true;
|
||
$scope.detectedType = "pr";
|
||
$scope.pullRequestId = $routeParams.pullRequestId;
|
||
$http.get("/api/pr/" + $scope.pullRequestId).then(
|
||
async (res) => {
|
||
$scope.sourceUrl = "https://github.com/" + res.data.source.repositoryFullName + "/pull/" + res.data.source.pullRequestId;
|
||
$scope.terms = res.data.options.terms.filter((f) => f).join("\n");
|
||
$scope.source = res.data.source;
|
||
$scope.options = Object.assign({}, $scope.options, res.data.options);
|
||
$scope.conference = res.data.conference;
|
||
if (res.data.options.expirationDate) {
|
||
$scope.options.expirationDate = new Date(res.data.options.expirationDate);
|
||
}
|
||
try {
|
||
$scope.details = (await $http.get(`/api/pr/${res.data.source.repositoryFullName}/${res.data.source.pullRequestId}`)).data;
|
||
} catch (error) {
|
||
const code = error && error.data && error.data.error;
|
||
if (code) {
|
||
$translate("ERRORS." + code).then((translation) => {
|
||
$scope.addToast({ title: "Error", date: new Date(), body: translation });
|
||
$scope.error = translation;
|
||
}, console.error);
|
||
displayErrorMessage(code);
|
||
}
|
||
}
|
||
$scope.$apply();
|
||
},
|
||
() => { $location.url("/404"); }
|
||
);
|
||
$scope.$watch("anonymize", () => {
|
||
if ($scope.anonymize.pullRequestId) $scope.anonymize.pullRequestId.$$element[0].disabled = true;
|
||
if ($scope.anonymize.sourceUrl) $scope.anonymize.sourceUrl.$$element[0].disabled = true;
|
||
});
|
||
}
|
||
// Edit mode: Gist
|
||
if ($routeParams.gistId && $routeParams.gistId != "") {
|
||
$scope.isUpdate = true;
|
||
$scope.detectedType = "gist";
|
||
$scope.gistId = $routeParams.gistId;
|
||
$http.get("/api/gist/" + $scope.gistId).then(
|
||
async (res) => {
|
||
$scope.sourceUrl = "https://gist.github.com/" + res.data.source.gistId;
|
||
$scope.terms = res.data.options.terms.filter((f) => f).join("\n");
|
||
$scope.source = res.data.source;
|
||
$scope.options = Object.assign({}, $scope.options, res.data.options);
|
||
$scope.conference = res.data.conference;
|
||
if (res.data.options.expirationDate) {
|
||
$scope.options.expirationDate = new Date(res.data.options.expirationDate);
|
||
}
|
||
$scope.details = (await $http.get(`/api/gist/source/${res.data.source.gistId}`)).data;
|
||
$scope.$apply();
|
||
},
|
||
() => { $location.url("/404"); }
|
||
);
|
||
$scope.$watch("anonymize", () => {
|
||
if ($scope.anonymize.gistId) $scope.anonymize.gistId.$$element[0].disabled = true;
|
||
if ($scope.anonymize.sourceUrl) $scope.anonymize.sourceUrl.$$element[0].disabled = true;
|
||
});
|
||
}
|
||
});
|
||
|
||
// URL change handler - auto-detect type
|
||
$scope.urlSelected = async () => {
|
||
$scope.terms = $scope.defaultTerms;
|
||
$scope.repoId = "";
|
||
$scope.pullRequestId = "";
|
||
$scope.gistId = "";
|
||
$scope.details = null;
|
||
$scope.branches = [];
|
||
$scope.source = { type: "GitHubStream", branch: "", commit: "" };
|
||
$scope.anonymize_readme = "";
|
||
$scope.readme = "";
|
||
$scope.html_readme = "";
|
||
$scope.detectedType = null;
|
||
|
||
let o;
|
||
try {
|
||
o = parseGithubUrl($scope.sourceUrl);
|
||
} catch (error) {
|
||
setValidity("sourceUrl", "github", false);
|
||
return;
|
||
}
|
||
setValidity("sourceUrl", "github", true);
|
||
try {
|
||
if (o.gistId && !o.repo) {
|
||
$scope.detectedType = "gist";
|
||
$scope.source = { gistId: o.gistId };
|
||
await getGistDetails();
|
||
} else if (o.pullRequestId) {
|
||
$scope.detectedType = "pr";
|
||
$scope.source = { repositoryFullName: o.owner + "/" + o.repo, pullRequestId: o.pullRequestId };
|
||
await getPrDetails();
|
||
} else {
|
||
$scope.detectedType = "repo";
|
||
await Promise.all([getRepoDetails(), getReadme()]);
|
||
anonymizeReadme();
|
||
}
|
||
} catch (error) {
|
||
return;
|
||
}
|
||
$scope.$apply();
|
||
$('[data-toggle="tooltip"]').tooltip();
|
||
};
|
||
$('[data-toggle="tooltip"]').tooltip();
|
||
|
||
// ========== REPO LOGIC ==========
|
||
$scope.$watch("options.update", (v) => {
|
||
if ($scope.detectedType !== "repo") return;
|
||
if ($scope.anonymize && $scope.anonymize.commit) {
|
||
$scope.anonymize.commit.$$element[0].disabled = !!v;
|
||
}
|
||
});
|
||
|
||
$scope.$watch("source.branch", async () => {
|
||
if ($scope.detectedType !== "repo") return;
|
||
const selected = $scope.branches.filter((f) => f.name == $scope.source.branch)[0];
|
||
if (!selected) return;
|
||
// In update mode, preserve the saved commit while the branch is
|
||
// unchanged — see #360. Saving the form (e.g. to turn off
|
||
// auto-update) used to bump the commit to GitHub HEAD because this
|
||
// watcher overwrote it on edit-page load.
|
||
const keepSavedCommit =
|
||
$scope.isUpdate &&
|
||
$scope._originalBranch === $scope.source.branch &&
|
||
!!$scope.source.commit;
|
||
if (!keepSavedCommit) {
|
||
$scope.source.commit = selected.commit;
|
||
}
|
||
$scope.readme = selected.readme;
|
||
await getReadme();
|
||
anonymizeReadme();
|
||
$scope.$apply();
|
||
});
|
||
|
||
$scope.getBranches = async (force) => {
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
try {
|
||
const branches = await $http.get(`/api/repo/${o.owner}/${o.repo}/branches`, {
|
||
params: { force: force === true ? "1" : "0", repositoryID: $scope.repositoryID },
|
||
});
|
||
$scope.branches = branches.data;
|
||
$scope.sourceUnreachable = false;
|
||
if (!$scope.source.branch) {
|
||
$scope.source.branch = $scope.details.defaultBranch;
|
||
}
|
||
const selected = $scope.branches.filter((b) => b.name == $scope.source.branch);
|
||
if (selected.length > 0) {
|
||
// When the user explicitly clicks refresh (force=true), always
|
||
// update the commit to the latest on the branch. Only preserve
|
||
// the saved commit on the initial edit-page load (#360).
|
||
const keepSavedCommit =
|
||
!force &&
|
||
$scope.isUpdate &&
|
||
!$scope.options.update &&
|
||
$scope._originalBranch === $scope.source.branch &&
|
||
!!$scope.source.commit;
|
||
if (!keepSavedCommit) {
|
||
$scope.source.commit = selected[0].commit;
|
||
}
|
||
$scope.readme = selected[0].readme;
|
||
await getReadme(force);
|
||
}
|
||
} catch (error) {
|
||
$scope.branches = [];
|
||
$scope.sourceUnreachable = error && (error.status === 404 || (error.data && error.data.error === "repo_not_found"));
|
||
const code = (error && error.data && error.data.error) || (error && error.status === 404 ? "repo_not_found" : "unknown_error");
|
||
$translate("ERRORS." + code).then((translation) => {
|
||
$scope.toasts = $scope.toasts || [];
|
||
$scope.addToast({ title: "Error", date: new Date(), body: translation });
|
||
$scope.error = translation;
|
||
}, console.error);
|
||
if (typeof setValidity === "function") {
|
||
setValidity("sourceUrl", "missing", false);
|
||
}
|
||
}
|
||
$scope.$apply();
|
||
};
|
||
|
||
async function getRepoDetails() {
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
try {
|
||
resetValidity();
|
||
// force=1 so newly enabled features (e.g. GitHub Pages — see
|
||
// #364) are reflected without waiting for the cached metadata to
|
||
// expire. The endpoint hits the GitHub API once.
|
||
const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/`, {
|
||
params: { repositoryID: $scope.repositoryID, force: "1" },
|
||
});
|
||
$scope.details = res.data;
|
||
if (!$scope.repoId) {
|
||
$scope.repoId = $scope.details.repo + "-" + generateRandomId(4);
|
||
}
|
||
await $scope.getBranches();
|
||
} catch (error) {
|
||
if (error.data) {
|
||
$translate("ERRORS." + error.data.error).then((translation) => {
|
||
$scope.addToast({ title: "Error", date: new Date(), body: translation });
|
||
$scope.error = translation;
|
||
}, console.error);
|
||
displayErrorMessage(error.data.error);
|
||
}
|
||
setValidity("sourceUrl", "missing", false);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function getReadme(force) {
|
||
if ($scope.readme && !force) return $scope.readme;
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
try {
|
||
const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/readme`, {
|
||
params: { force: force === true ? "1" : "0", branch: $scope.source.branch, repositoryID: $scope.repositoryID },
|
||
});
|
||
$scope.readme = res.data;
|
||
} catch (error) {
|
||
$scope.readme = "";
|
||
}
|
||
}
|
||
|
||
// Both anonymizeReadme() and anonymizePrContent() used to reimplement
|
||
// ContentAnonimizer client-side, which drifted from the backend (term
|
||
// boundary fixes, accent matching, custom replacements all only landed
|
||
// in the server). Send the snippets to /api/anonymize-preview instead so
|
||
// the preview matches what reviewers see byte-for-byte. Calls are
|
||
// debounced and the in-flight request is dropped on the next change so
|
||
// typing in the form stays responsive.
|
||
|
||
function previewOptions() {
|
||
const opts = {
|
||
terms: $scope.terms ? $scope.terms.split("\n") : [],
|
||
image: !!$scope.options.image,
|
||
link: !!$scope.options.link,
|
||
repoId: $scope.repoId,
|
||
};
|
||
if ($scope.source && $scope.source.branch) {
|
||
opts.branchName = $scope.source.branch;
|
||
}
|
||
try {
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
opts.repoName = `${o.owner}/${o.repo}`;
|
||
} catch (_) { /* sourceUrl not yet parseable */ }
|
||
return opts;
|
||
}
|
||
|
||
// Single-flight + debounced wrapper. Returns a promise that resolves
|
||
// with the latest server result; intermediate calls are coalesced.
|
||
function makePreviewBatcher(buildBody, applyResult) {
|
||
let pendingTimer = null;
|
||
let inflightToken = 0;
|
||
return function schedule() {
|
||
if (pendingTimer) $timeout.cancel(pendingTimer);
|
||
pendingTimer = $timeout(() => {
|
||
pendingTimer = null;
|
||
const myToken = ++inflightToken;
|
||
const body = buildBody();
|
||
if (!body) return;
|
||
$http.post("/api/anonymize-preview", body).then(
|
||
(res) => {
|
||
if (myToken !== inflightToken) return; // stale
|
||
applyResult(res.data);
|
||
},
|
||
() => { /* ignore preview errors; no UI feedback needed */ }
|
||
);
|
||
}, 200);
|
||
};
|
||
}
|
||
|
||
const scheduleReadmePreview = makePreviewBatcher(
|
||
() => {
|
||
if (!$scope.readme) return null;
|
||
return { content: $scope.readme, options: previewOptions() };
|
||
},
|
||
(data) => {
|
||
$scope.anonymize_readme = data.content || "";
|
||
let baseUrl = "";
|
||
try {
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
// Fall back to the repo's default branch when source.branch
|
||
// hasn't loaded yet — without this, relative <img src="./X">
|
||
// resolved against a baseUrl like ".../raw//" (no branch
|
||
// segment), so the browser fetched ".../raw/X" and 404'd
|
||
// (#407).
|
||
const branch =
|
||
$scope.source.branch ||
|
||
($scope.details && $scope.details.defaultBranch) ||
|
||
"main";
|
||
baseUrl = `https://github.com/${o.owner}/${o.repo}/raw/${branch}/`;
|
||
} catch (_) { /* fall through with empty base */ }
|
||
const html = renderMD($scope.anonymize_readme, baseUrl);
|
||
$scope.html_readme = $sce.trustAsHtml(html);
|
||
$timeout(Prism.highlightAll, 150);
|
||
}
|
||
);
|
||
|
||
function anonymizeReadme() {
|
||
if (!$scope.anonymize || !$scope.anonymize.terms) return;
|
||
// The "regex characters detected" hint is informational, not a blocker
|
||
// — IP addresses, escaped chars, etc. are all legitimate terms (#430).
|
||
$scope.termsRegexWarning =
|
||
!!$scope.terms && !!$scope.terms.match(/[-[\]{}()*+?.,\\^$|#]/g);
|
||
scheduleReadmePreview();
|
||
}
|
||
|
||
// ========== PR LOGIC ==========
|
||
async function getPrDetails() {
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
try {
|
||
resetValidity();
|
||
const res = await $http.get(`/api/pr/${o.owner}/${o.repo}/${o.pullRequestId}`);
|
||
$scope.details = res.data;
|
||
if (!$scope.pullRequestId) {
|
||
$scope.pullRequestId = o.repo + "-PR" + o.pullRequestId + "-" + generateRandomId(4);
|
||
}
|
||
} catch (error) {
|
||
if (error.data) {
|
||
$translate("ERRORS." + error.data.error).then((translation) => {
|
||
$scope.addToast({ title: "Error", date: new Date(), body: translation });
|
||
$scope.error = translation;
|
||
}, console.error);
|
||
displayErrorMessage(error.data.error);
|
||
}
|
||
setValidity("sourceUrl", "missing", false);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Angular templates evaluate this synchronously, so we keep a
|
||
// {original -> anonymized} cache populated by a debounced batch call to
|
||
// /api/anonymize-preview whenever the PR details, terms, or options
|
||
// change. anonymizePrContent() returns the cached value if known and
|
||
// falls back to the original until the next cycle resolves.
|
||
let _prAnonCache = new Map();
|
||
let _prSeenContents = new Set();
|
||
|
||
function collectPrContents() {
|
||
const out = new Set();
|
||
const d = $scope.details && $scope.details.pullRequest;
|
||
if (!d) return out;
|
||
if (typeof d.title === "string") out.add(d.title);
|
||
if (typeof d.body === "string") out.add(d.body);
|
||
if (typeof d.diff === "string") out.add(d.diff);
|
||
const comments =
|
||
($scope.details && $scope.details.comments) || [];
|
||
for (const c of comments) {
|
||
if (typeof c.author === "string") out.add(c.author);
|
||
if (typeof c.body === "string") out.add(c.body);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const refreshPrPreview = makePreviewBatcher(
|
||
() => {
|
||
const seen = collectPrContents();
|
||
_prSeenContents = seen;
|
||
const list = Array.from(seen);
|
||
if (list.length === 0) return null;
|
||
return { contents: list, options: previewOptions() };
|
||
},
|
||
(data) => {
|
||
if (!data || !Array.isArray(data.contents)) return;
|
||
const seen = Array.from(_prSeenContents);
|
||
const next = new Map();
|
||
for (let i = 0; i < seen.length && i < data.contents.length; i++) {
|
||
next.set(seen[i], data.contents[i]);
|
||
}
|
||
_prAnonCache = next;
|
||
}
|
||
);
|
||
|
||
$scope.anonymizePrContent = function (content) {
|
||
if (!content) return content;
|
||
if (_prAnonCache.has(content)) return _prAnonCache.get(content);
|
||
if (!_prSeenContents.has(content)) {
|
||
refreshPrPreview();
|
||
}
|
||
return content;
|
||
};
|
||
|
||
// ========== GIST LOGIC ==========
|
||
async function getGistDetails() {
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
try {
|
||
resetValidity();
|
||
const res = await $http.get(`/api/gist/source/${o.gistId}`);
|
||
$scope.details = res.data;
|
||
if (!$scope.gistId) {
|
||
$scope.gistId = "gist-" + o.gistId.substring(0, 6) + "-" + generateRandomId(4);
|
||
}
|
||
} catch (error) {
|
||
if (error.data) {
|
||
$translate("ERRORS." + error.data.error).then((translation) => {
|
||
$scope.addToast({ title: "Error", date: new Date(), body: translation });
|
||
$scope.error = translation;
|
||
}, console.error);
|
||
displayErrorMessage(error.data.error);
|
||
}
|
||
setValidity("sourceUrl", "missing", false);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
let _gistAnonCache = new Map();
|
||
let _gistSeenContents = new Set();
|
||
let _gistCacheVersion = 0;
|
||
|
||
function collectGistContents() {
|
||
const out = new Set();
|
||
const d = $scope.details && $scope.details.gist;
|
||
if (!d) return out;
|
||
if (typeof d.description === "string") out.add(d.description);
|
||
if (typeof d.ownerLogin === "string") out.add(d.ownerLogin);
|
||
const files = (d.files) || [];
|
||
for (const f of files) {
|
||
if (typeof f.filename === "string") out.add(f.filename);
|
||
if (typeof f.content === "string") out.add(f.content);
|
||
}
|
||
const comments = d.comments || [];
|
||
for (const c of comments) {
|
||
if (typeof c.author === "string") out.add(c.author);
|
||
if (typeof c.body === "string") out.add(c.body);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const refreshGistPreview = makePreviewBatcher(
|
||
() => {
|
||
const seen = collectGistContents();
|
||
_gistSeenContents = seen;
|
||
const list = Array.from(seen);
|
||
if (list.length === 0) return null;
|
||
return { contents: list, options: previewOptions() };
|
||
},
|
||
(data) => {
|
||
if (!data || !Array.isArray(data.contents)) return;
|
||
const seen = Array.from(_gistSeenContents);
|
||
const next = new Map();
|
||
for (let i = 0; i < seen.length && i < data.contents.length; i++) {
|
||
next.set(seen[i], data.contents[i]);
|
||
}
|
||
_gistAnonCache = next;
|
||
_gistCacheVersion++;
|
||
rebuildPreviewGistFiles();
|
||
}
|
||
);
|
||
|
||
$scope.anonymizeGistContent = function (content) {
|
||
if (!content) return content;
|
||
if (_gistAnonCache.has(content)) return _gistAnonCache.get(content);
|
||
if (!_gistSeenContents.has(content)) {
|
||
refreshGistPreview();
|
||
}
|
||
return content;
|
||
};
|
||
|
||
// Precomputed file objects for the preview pane so <gist-file>'s
|
||
// two-way binding has a stable reference. Recomputes when the source
|
||
// files change OR when the anonymization cache turns over.
|
||
$scope.previewGistFiles = [];
|
||
function rebuildPreviewGistFiles() {
|
||
const files =
|
||
($scope.details && $scope.details.gist && $scope.details.gist.files) || [];
|
||
$scope.previewGistFiles = files.map((f) => ({
|
||
filename: $scope.anonymizeGistContent(f.filename),
|
||
content: $scope.anonymizeGistContent(f.content),
|
||
language: f.language,
|
||
}));
|
||
}
|
||
// _prAnonCache turns over inside refreshGistPreview's applyResult; the
|
||
// simplest signal we have is the digest cycle, so re-derive each digest.
|
||
// Cheap when _gistAnonCache hits.
|
||
$scope.$watch("details.gist.files", rebuildPreviewGistFiles, true);
|
||
$scope.$watch("terms", rebuildPreviewGistFiles);
|
||
|
||
// ========== SHARED LOGIC ==========
|
||
function getConference() {
|
||
if (!$scope.conference) return;
|
||
$http.get("/api/conferences/" + $scope.conference).then(
|
||
(res) => {
|
||
$scope.conference_data = res.data;
|
||
$scope.conference_data.startDate = new Date($scope.conference_data.startDate);
|
||
$scope.conference_data.endDate = new Date($scope.conference_data.endDate);
|
||
$scope.options.expirationDate = new Date($scope.conference_data.endDate);
|
||
$scope.options.expirationMode = "remove";
|
||
$scope.options.update = $scope.conference_data.options.update;
|
||
$scope.options.image = $scope.conference_data.options.image;
|
||
$scope.options.pdf = $scope.conference_data.options.pdf;
|
||
$scope.options.notebook = $scope.conference_data.options.notebook;
|
||
$scope.options.link = $scope.conference_data.options.link;
|
||
},
|
||
() => { $scope.conference_data = null; }
|
||
);
|
||
}
|
||
|
||
function resetValidity() {
|
||
setValidity("repoId", "used", true);
|
||
setValidity("repoId", "format", true);
|
||
setValidity("pullRequestId", "used", true);
|
||
setValidity("pullRequestId", "format", true);
|
||
setValidity("gistId", "used", true);
|
||
setValidity("gistId", "format", true);
|
||
setValidity("sourceUrl", "used", true);
|
||
setValidity("sourceUrl", "missing", true);
|
||
setValidity("sourceUrl", "access", true);
|
||
setValidity("sourceUrl", "github", true);
|
||
setValidity("commit", "exists", true);
|
||
setValidity("conference", "activated", true);
|
||
setValidity("terms", "format", true);
|
||
$scope.termsRegexWarning = false;
|
||
}
|
||
|
||
function displayErrorMessage(message) {
|
||
const idField =
|
||
$scope.detectedType === "pr"
|
||
? "pullRequestId"
|
||
: $scope.detectedType === "gist"
|
||
? "gistId"
|
||
: "repoId";
|
||
switch (message) {
|
||
case "repoId_already_used": setValidity(idField, "used", false); break;
|
||
case "invalid_repoId": setValidity(idField, "format", false); break;
|
||
case "options_not_provided": setValidity(idField, "format", false); break;
|
||
case "repo_already_anonymized": setValidity("sourceUrl", "used", false); break;
|
||
case "invalid_terms_format": setValidity("terms", "format", false); break;
|
||
case "repo_not_found": setValidity("sourceUrl", "missing", false); break;
|
||
case "repo_not_accessible": setValidity("sourceUrl", "access", false); break;
|
||
case "commit_not_found": setValidity("commit", "exists", false); break;
|
||
case "conf_not_activated": setValidity("conference", "activated", false); break;
|
||
}
|
||
}
|
||
|
||
// ========== CO-AUTHORS ==========
|
||
$scope.coauthors = $scope.coauthors || [];
|
||
$scope.coauthorResults = [];
|
||
$scope.coauthorError = "";
|
||
|
||
$scope.searchCoauthors = () => {
|
||
const q = ($scope.coauthorSearch || "").trim();
|
||
$scope.coauthorError = "";
|
||
if (q.length < 2) {
|
||
$scope.coauthorResults = [];
|
||
return;
|
||
}
|
||
$http.get("/api/user/search/github-users", { params: { q } }).then(
|
||
(res) => {
|
||
const existing = new Set(
|
||
($scope.coauthors || []).map((c) => (c.username || "").toLowerCase())
|
||
);
|
||
$scope.coauthorResults = (res.data || []).filter(
|
||
(u) => !existing.has((u.username || "").toLowerCase())
|
||
);
|
||
},
|
||
() => { $scope.coauthorResults = []; }
|
||
);
|
||
};
|
||
|
||
$scope.addCoauthor = (u, event) => {
|
||
if (event) event.preventDefault();
|
||
if (!u || !u.username) return;
|
||
$http
|
||
.post("/api/repo/" + $scope.repoId + "/coauthors", {
|
||
username: u.username,
|
||
})
|
||
.then(
|
||
(res) => {
|
||
$scope.coauthors = res.data || [];
|
||
$scope.coauthorResults = [];
|
||
$scope.coauthorSearch = "";
|
||
$scope.coauthorError = "";
|
||
},
|
||
(err) => {
|
||
const code = (err && err.data && err.data.error) || "unknown_error";
|
||
$scope.coauthorError = code;
|
||
}
|
||
);
|
||
};
|
||
|
||
$scope.removeCoauthor = (c) => {
|
||
if (!c || !c.username) return;
|
||
if (!confirm("Remove co-author " + c.username + "?")) return;
|
||
$http
|
||
.delete(
|
||
"/api/repo/" +
|
||
$scope.repoId +
|
||
"/coauthors/" +
|
||
encodeURIComponent(c.username)
|
||
)
|
||
.then(
|
||
(res) => { $scope.coauthors = res.data || []; },
|
||
(err) => {
|
||
const code = (err && err.data && err.data.error) || "unknown_error";
|
||
$scope.coauthorError = code;
|
||
}
|
||
);
|
||
};
|
||
|
||
// Submit: repo
|
||
$scope.anonymizeRepo = (event) => {
|
||
event.target.disabled = true;
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
const payload = {
|
||
repoId: $scope.repoId,
|
||
terms: $scope.terms.trim().split("\n").filter((f) => f),
|
||
fullName: `${o.owner}/${o.repo}`,
|
||
repository: $scope.sourceUrl,
|
||
options: $scope.options,
|
||
source: $scope.source,
|
||
conference: $scope.conference,
|
||
};
|
||
if ($scope.details) payload.options.pageSource = $scope.details.pageSource;
|
||
resetValidity();
|
||
const url = $scope.isUpdate ? "/api/repo/" + $scope.repoId : "/api/repo/";
|
||
$http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then(
|
||
() => { window.location.href = "/status/" + $scope.repoId; },
|
||
(error) => {
|
||
if (error.data) {
|
||
$translate("ERRORS." + error.data.error).then((t) => { $scope.error = t; }, console.error);
|
||
displayErrorMessage(error.data.error);
|
||
}
|
||
}
|
||
).finally(() => { event.target.disabled = false; $scope.$apply(); });
|
||
};
|
||
|
||
// Submit: Gist
|
||
$scope.anonymizeGist = (event) => {
|
||
event.target.disabled = true;
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
const payload = {
|
||
gistId: $scope.gistId,
|
||
terms: $scope.terms.trim().split("\n").filter((f) => f),
|
||
source: { gistId: o.gistId },
|
||
options: $scope.options,
|
||
conference: $scope.conference,
|
||
};
|
||
resetValidity();
|
||
const url = $scope.isUpdate ? "/api/gist/" + $scope.gistId : "/api/gist/";
|
||
$http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then(
|
||
() => { window.location.href = "/gist/" + $scope.gistId; },
|
||
(error) => {
|
||
if (error.data) {
|
||
$translate("ERRORS." + error.data.error).then((t) => { $scope.error = t; }, console.error);
|
||
displayErrorMessage(error.data.error);
|
||
}
|
||
}
|
||
).finally(() => { event.target.disabled = false; $scope.$apply(); });
|
||
};
|
||
|
||
// Submit: PR
|
||
$scope.anonymizePullRequest = (event) => {
|
||
event.target.disabled = true;
|
||
const o = parseGithubUrl($scope.sourceUrl);
|
||
const payload = {
|
||
pullRequestId: $scope.pullRequestId,
|
||
terms: $scope.terms.trim().split("\n").filter((f) => f),
|
||
source: { repositoryFullName: `${o.owner}/${o.repo}`, pullRequestId: o.pullRequestId },
|
||
options: $scope.options,
|
||
conference: $scope.conference,
|
||
};
|
||
resetValidity();
|
||
const url = $scope.isUpdate ? "/api/pr/" + $scope.pullRequestId : "/api/pr/";
|
||
$http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then(
|
||
() => { window.location.href = "/pr/" + $scope.pullRequestId; },
|
||
(error) => {
|
||
if (error.data) {
|
||
$translate("ERRORS." + error.data.error).then((t) => { $scope.error = t; }, console.error);
|
||
displayErrorMessage(error.data.error);
|
||
}
|
||
}
|
||
).finally(() => { event.target.disabled = false; $scope.$apply(); });
|
||
};
|
||
|
||
$scope.$watch("conference", () => { getConference(); });
|
||
$scope.$watch("terms", () => {
|
||
if ($scope.detectedType === "repo") anonymizeReadme();
|
||
if ($scope.detectedType === "pr") refreshPrPreview();
|
||
if ($scope.detectedType === "gist") refreshGistPreview();
|
||
});
|
||
$scope.$watch("options.image", () => {
|
||
if ($scope.detectedType === "repo") anonymizeReadme();
|
||
if ($scope.detectedType === "pr") refreshPrPreview();
|
||
if ($scope.detectedType === "gist") refreshGistPreview();
|
||
});
|
||
$scope.$watch("options.link", () => {
|
||
if ($scope.detectedType === "repo") anonymizeReadme();
|
||
if ($scope.detectedType === "pr") refreshPrPreview();
|
||
if ($scope.detectedType === "gist") refreshGistPreview();
|
||
});
|
||
$scope.$watch("details", () => {
|
||
if ($scope.detectedType === "pr") refreshPrPreview();
|
||
if ($scope.detectedType === "gist") refreshGistPreview();
|
||
}, true);
|
||
},
|
||
])
|
||
.controller("exploreController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
"$routeParams",
|
||
"$sce",
|
||
"$q",
|
||
"PDFViewerService",
|
||
function ($scope, $http, $location, $routeParams, $sce, $q, PDFViewerService) {
|
||
$scope.files = [];
|
||
$scope.isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
|
||
$scope.fileSearchQuery = "";
|
||
$scope.fileSearchResults = null;
|
||
$scope.fileSearchLoading = false;
|
||
|
||
document.addEventListener("keydown", function (e) {
|
||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||
e.preventDefault();
|
||
var input = document.querySelector(".tree-search-input");
|
||
if (input) {
|
||
input.focus();
|
||
input.select();
|
||
}
|
||
}
|
||
});
|
||
var searchCanceller = null;
|
||
$scope.onFileSearchChange = function () {
|
||
// Cancel any in-flight search request
|
||
if (searchCanceller) {
|
||
searchCanceller.resolve();
|
||
searchCanceller = null;
|
||
}
|
||
const query = $scope.fileSearchQuery;
|
||
if (!query || query.length < 2) {
|
||
$scope.fileSearchResults = null;
|
||
$scope.fileSearchLoading = false;
|
||
return;
|
||
}
|
||
$scope.fileSearchLoading = true;
|
||
searchCanceller = $q.defer();
|
||
$http.get(
|
||
`/api/repo/${$scope.repoId}/files/search?q=${encodeURIComponent(query)}`,
|
||
{ timeout: searchCanceller.promise }
|
||
).then(function (res) {
|
||
searchCanceller = null;
|
||
$scope.fileSearchLoading = false;
|
||
// Merge search results into $scope.files so the tree can render them.
|
||
// Ancestor folders must appear before their children for toArray() to work.
|
||
var existing = {};
|
||
$scope.files.forEach(function(f) {
|
||
existing[(f.path || "") + "/" + f.name] = true;
|
||
});
|
||
// First pass: collect ancestor folders (shallow to deep)
|
||
var foldersToAdd = [];
|
||
var folderSeen = {};
|
||
for (var i = 0; i < res.data.length; i++) {
|
||
var f = res.data[i];
|
||
if (f.path) {
|
||
var segments = f.path.split("/");
|
||
var acc = "";
|
||
for (var j = 0; j < segments.length; j++) {
|
||
var parent = acc;
|
||
acc = acc ? acc + "/" + segments[j] : segments[j];
|
||
var folderKey = parent + "/" + segments[j];
|
||
if (!existing[folderKey] && !folderSeen[folderKey]) {
|
||
folderSeen[folderKey] = true;
|
||
foldersToAdd.push({ name: segments[j], path: parent });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Sort folders by depth (shallow first)
|
||
foldersToAdd.sort(function(a, b) {
|
||
return (a.path || "").split("/").length - (b.path || "").split("/").length;
|
||
});
|
||
// Add folders first, then files
|
||
if (foldersToAdd.length > 0) {
|
||
$scope.files.push.apply($scope.files, foldersToAdd);
|
||
}
|
||
var filesToAdd = [];
|
||
for (var k = 0; k < res.data.length; k++) {
|
||
var rf = res.data[k];
|
||
var key = (rf.path || "") + "/" + rf.name;
|
||
if (!existing[key] && rf.size != null) {
|
||
filesToAdd.push(rf);
|
||
existing[key] = true;
|
||
}
|
||
}
|
||
if (filesToAdd.length > 0) {
|
||
$scope.files.push.apply($scope.files, filesToAdd);
|
||
}
|
||
$scope.fileSearchResults = res.data;
|
||
}, function () {
|
||
// Only clear loading if this wasn't a cancellation
|
||
if (!searchCanceller) {
|
||
$scope.fileSearchLoading = false;
|
||
$scope.fileSearchResults = [];
|
||
}
|
||
});
|
||
};
|
||
const extensionModes = {
|
||
yml: "yaml",
|
||
txt: "text",
|
||
py: "python",
|
||
js: "javascript",
|
||
ts: "typescript",
|
||
};
|
||
const textFiles = ["license", "txt"];
|
||
const imageFiles = [
|
||
"png",
|
||
"jpg",
|
||
"jpeg",
|
||
"gif",
|
||
"svg",
|
||
"ico",
|
||
"bmp",
|
||
"tiff",
|
||
"tif",
|
||
"webp",
|
||
"avif",
|
||
"heif",
|
||
"heic",
|
||
];
|
||
const audioFiles = ["wav", "mp3", "ogg", "wma", "flac", "aac", "m4a"];
|
||
const mediaFiles = [
|
||
"mp4",
|
||
"avi",
|
||
"webm",
|
||
"mov",
|
||
"mpg",
|
||
"mpeg",
|
||
"mkv",
|
||
"flv",
|
||
"wmv",
|
||
"3gp",
|
||
"3g2",
|
||
"m4v",
|
||
"f4v",
|
||
"f4p",
|
||
"f4a",
|
||
"f4b",
|
||
];
|
||
|
||
$scope.$on("$routeUpdate", function (event, current) {
|
||
if (($routeParams.path || "") == $scope.filePath) {
|
||
return;
|
||
}
|
||
$scope.filePath = $routeParams.path || "";
|
||
$scope.paths = $scope.filePath
|
||
.split("/")
|
||
.filter((f) => f && f.trim().length > 0);
|
||
|
||
if ($scope.repoId != $routeParams.repoId) {
|
||
return init();
|
||
}
|
||
|
||
updateContent();
|
||
|
||
// #510 — if we navigated into a subdirectory whose file listing
|
||
// hasn't been fetched, lazy-load the parent directories in the
|
||
// background so getSelectedFile() can populate $scope.file with the
|
||
// right sha for the next interaction. Done after updateContent so
|
||
// the request fires immediately (getContent falls back to sha "0").
|
||
for (let i = 0; i < $scope.paths.length - 1; i++) {
|
||
const dirPath = i > 0 ? $scope.paths.slice(0, i).join("/") : "";
|
||
const alreadyLoaded = $scope.files.some((f) => f.path === dirPath);
|
||
if (!alreadyLoaded) {
|
||
$scope.getFiles(dirPath);
|
||
}
|
||
}
|
||
});
|
||
|
||
function selectFile() {
|
||
if ($scope.paths[0] != "") {
|
||
return;
|
||
}
|
||
const readmePriority = [
|
||
"readme.md",
|
||
"readme.txt",
|
||
"readme.org",
|
||
"readme.1st",
|
||
"readme",
|
||
];
|
||
const readmeCandidates = {};
|
||
for (const file of $scope.files) {
|
||
if (file.name.toLowerCase().indexOf("readme") > -1) {
|
||
readmeCandidates[file.name.toLowerCase()] = file.name;
|
||
}
|
||
}
|
||
let best_match = null;
|
||
for (const p of readmePriority) {
|
||
if (readmeCandidates[p]) {
|
||
best_match = p;
|
||
break;
|
||
}
|
||
}
|
||
if (!best_match && Object.keys(readmeCandidates).length > 0)
|
||
best_match = Object.keys(readmeCandidates)[0];
|
||
if (best_match) {
|
||
let uri = $location.url();
|
||
if (uri[uri.length - 1] != "/") {
|
||
uri += "/";
|
||
}
|
||
|
||
// redirect to readme
|
||
$location.url(
|
||
uri + encodePathForUrl(readmeCandidates[best_match])
|
||
);
|
||
}
|
||
}
|
||
$scope.fileCounts = null;
|
||
$scope.getFiles = function (path) {
|
||
return $http.get(
|
||
`/api/repo/${$scope.repoId}/files/?path=${encodeURIComponent(path)}&v=${$scope.options.lastUpdateDate}`
|
||
).then(function (res) {
|
||
const normalized = path || "";
|
||
$scope.files = $scope.files.filter((f) => f.path !== normalized);
|
||
$scope.files.push(...res.data);
|
||
return res.data;
|
||
}, function (err) {
|
||
$scope.type = "error";
|
||
$scope.content = (err && err.data && err.data.error) || "unknown_error";
|
||
$scope.files = [];
|
||
});
|
||
};
|
||
function fetchFileCounts() {
|
||
$http.get(
|
||
`/api/repo/${$scope.repoId}/files/counts`
|
||
).then(function (res) {
|
||
$scope.fileCounts = res.data;
|
||
}, function () {
|
||
$scope.fileCounts = {};
|
||
});
|
||
}
|
||
|
||
function getSelectedFile() {
|
||
return $scope.files.filter(
|
||
(f) =>
|
||
f.name == $scope.paths[$scope.paths.length - 1] &&
|
||
f.path == $scope.paths.slice(0, $scope.paths.length - 1).join("/")
|
||
)[0];
|
||
}
|
||
|
||
var rlCountdownTimer = null;
|
||
$scope.$on("$destroy", function () { if (rlCountdownTimer) clearInterval(rlCountdownTimer); });
|
||
|
||
function getOptions(callback) {
|
||
$http.get(`/api/repo/${$scope.repoId}/options`).then(
|
||
(res) => {
|
||
$scope.options = res.data;
|
||
if ($scope.options.url) {
|
||
window.location = $scope.options.url;
|
||
return;
|
||
}
|
||
if (callback) {
|
||
callback(res.data);
|
||
}
|
||
},
|
||
(err) => {
|
||
var data = err.data || {};
|
||
if (data.error === "rate_limited" && data.resetAt) {
|
||
$scope.type = "rate_limited";
|
||
$scope.rateLimitResetAt = data.resetAt;
|
||
if (rlCountdownTimer) clearInterval(rlCountdownTimer);
|
||
function rlTick() {
|
||
var remaining = Math.max(0, Math.ceil(($scope.rateLimitResetAt - Date.now()) / 1000));
|
||
if (remaining <= 0) {
|
||
$scope.rateLimitCountdown = "";
|
||
$scope.rateLimitResetAt = 0;
|
||
if (rlCountdownTimer) { clearInterval(rlCountdownTimer); rlCountdownTimer = null; }
|
||
getOptions(callback);
|
||
} else {
|
||
var min = Math.floor(remaining / 60);
|
||
var sec = remaining % 60;
|
||
$scope.rateLimitCountdown = min > 0 ? min + "m " + sec + "s" : sec + "s";
|
||
}
|
||
$scope.$applyAsync();
|
||
}
|
||
rlTick();
|
||
rlCountdownTimer = setInterval(rlTick, 1000);
|
||
} else if (data.error === "repository_not_ready") {
|
||
$scope.type = "loading";
|
||
setTimeout(function () { getOptions(callback); }, 3000);
|
||
} else {
|
||
$scope.type = "error";
|
||
$scope.content = data.error;
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
function getMode(extension) {
|
||
if (extensionModes[extension]) {
|
||
return extensionModes[extension];
|
||
}
|
||
return extension;
|
||
}
|
||
|
||
function getType(extension) {
|
||
if (extension == "pdf") {
|
||
$scope.instance = PDFViewerService.Instance("viewer");
|
||
return "pdf";
|
||
}
|
||
if (extension == "md") {
|
||
return "md";
|
||
}
|
||
if (extension == "org") {
|
||
return "org";
|
||
}
|
||
if (extension == "ipynb") {
|
||
return "IPython";
|
||
}
|
||
if (textFiles.indexOf(extension) > -1) {
|
||
return "text";
|
||
}
|
||
if (imageFiles.indexOf(extension) > -1) {
|
||
return "image";
|
||
}
|
||
if (mediaFiles.indexOf(extension) > -1) {
|
||
return "media";
|
||
}
|
||
if (audioFiles.indexOf(extension) > -1) {
|
||
return "audio";
|
||
}
|
||
return "code";
|
||
}
|
||
|
||
function getContent(path, fileInfo) {
|
||
if (!path) {
|
||
$scope.type = "error";
|
||
$scope.content = "no_file_selected";
|
||
return;
|
||
}
|
||
const originalType = $scope.type;
|
||
$scope.type = "loading";
|
||
$scope.content = "loading";
|
||
// fileInfo can be undefined when the user navigates (e.g. clicks a
|
||
// markdown link into a subdir whose file list hasn't loaded yet) —
|
||
// see #510. Fall back to "0" so the request still goes through; the
|
||
// server returns a fresh ETag on first hit either way.
|
||
const sha = (fileInfo && fileInfo.sha) || "0";
|
||
$http
|
||
.get(
|
||
`/api/repo/${$scope.repoId}/file/${encodePathForUrl(path)}?v=` +
|
||
sha,
|
||
{
|
||
transformResponse: (data) => {
|
||
return data;
|
||
},
|
||
}
|
||
)
|
||
.then(
|
||
(res) => {
|
||
$scope.type = originalType;
|
||
$scope.content = res.data;
|
||
if ($scope.content == "") {
|
||
$scope.content = null;
|
||
}
|
||
|
||
if ($scope.type == "md") {
|
||
$scope.content = $sce.trustAsHtml(
|
||
renderMD(res.data, $location.url() + "/../")
|
||
);
|
||
$scope.type = "html";
|
||
}
|
||
if ($scope.type == "org") {
|
||
const content = contentAbs2Relative(res.data);
|
||
|
||
const orgParser = new Org.Parser();
|
||
const orgDocument = orgParser.parse(content);
|
||
var orgHTMLDocument = orgDocument.convert(Org.ConverterHTML, {
|
||
headerOffset: 1,
|
||
exportFromLineNumber: false,
|
||
suppressSubScriptHandling: true,
|
||
suppressAutoLink: false,
|
||
});
|
||
$scope.content = $sce.trustAsHtml(orgHTMLDocument.toString());
|
||
$scope.type = "html";
|
||
}
|
||
if (
|
||
$scope.type == "code" &&
|
||
res.headers("content-type") == "application/octet-stream"
|
||
) {
|
||
$scope.type = "binary";
|
||
$scope.content = "binary";
|
||
}
|
||
setTimeout(() => {
|
||
Prism.highlightAll();
|
||
}, 50);
|
||
},
|
||
(err) => {
|
||
$scope.type = "error";
|
||
$scope.content = "unknown_error";
|
||
try {
|
||
err.data = JSON.parse(err.data);
|
||
if (err.data.error) {
|
||
$scope.content = err.data.error;
|
||
} else {
|
||
$scope.content = err.data;
|
||
}
|
||
} catch (ignore) {
|
||
console.log(err);
|
||
if (err.status == -1) {
|
||
$scope.content = "request_error";
|
||
} else if (err.status == 502) {
|
||
// cloudflare error
|
||
$scope.content = "unreachable";
|
||
}
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
function updateContent() {
|
||
$scope.content = "";
|
||
$scope.file = getSelectedFile();
|
||
let fileVersion = "0";
|
||
if ($scope.file && $scope.file.sha) {
|
||
fileVersion = $scope.file.sha;
|
||
}
|
||
$scope.url = `/api/repo/${$scope.repoId}/file/${encodePathForUrl(
|
||
$scope.filePath
|
||
)}?v=${fileVersion}`;
|
||
|
||
let extension = $scope.filePath.toLowerCase();
|
||
const extensionIndex = extension.lastIndexOf(".");
|
||
if (extensionIndex > -1) {
|
||
extension = extension.substring(extensionIndex + 1);
|
||
}
|
||
|
||
$scope.aceOption = {
|
||
readOnly: true,
|
||
useWrapMode: true,
|
||
showGutter: true,
|
||
theme: "chrome",
|
||
useSoftTab: true,
|
||
showPrintMargin: true,
|
||
tabSize: 2,
|
||
highlightSelectedWord: true,
|
||
fontSize: 15,
|
||
keyBinding: "vscode",
|
||
fullLineSelection: true,
|
||
highlightActiveLine: false,
|
||
highlightGutterLine: false,
|
||
cursor: "hide",
|
||
showInvisibles: false,
|
||
showIndentGuides: true,
|
||
showPrintMargin: false,
|
||
highlightSelectedWord: false,
|
||
enableBehaviours: true,
|
||
fadeFoldWidgets: false,
|
||
mode: getMode(extension),
|
||
|
||
onLoad: function (_editor) {
|
||
const Range = ace.require("ace/range").Range;
|
||
let activeLineMarker = null;
|
||
|
||
function highlightLines(from, to) {
|
||
if (activeLineMarker !== null) {
|
||
_editor.session.removeMarker(activeLineMarker);
|
||
activeLineMarker = null;
|
||
}
|
||
if (from === null || from === undefined) return;
|
||
activeLineMarker = _editor.session.addMarker(
|
||
new Range(from, 0, to, 1),
|
||
"highlighted-line",
|
||
"fullLine"
|
||
);
|
||
}
|
||
|
||
function applyHashFromUrl(scroll) {
|
||
const m = window.location.hash.match(/^#L(\d+)(?:-L(\d+))?/);
|
||
if (!m) {
|
||
highlightLines(null);
|
||
return;
|
||
}
|
||
const from = parseInt(m[1]) - 1;
|
||
const to = m[2] ? parseInt(m[2]) - 1 : from;
|
||
highlightLines(from, to);
|
||
if (scroll) {
|
||
setTimeout(() => {
|
||
_editor.scrollToLine(from, true, true, function () {});
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
applyHashFromUrl(true);
|
||
|
||
// #392 — clicking a gutter line updates the URL to #L<n> and
|
||
// shift-clicking extends to #L<from>-L<to> so the user can copy
|
||
// a stable link to a specific line. Use replaceState to avoid
|
||
// polluting history with every click.
|
||
let anchorRow = null;
|
||
_editor.on("guttermousedown", function (e) {
|
||
const row = e.getDocumentPosition().row;
|
||
const shift = e.domEvent && e.domEvent.shiftKey;
|
||
let from = row;
|
||
let to = row;
|
||
if (shift && anchorRow !== null) {
|
||
from = Math.min(anchorRow, row);
|
||
to = Math.max(anchorRow, row);
|
||
} else {
|
||
anchorRow = row;
|
||
}
|
||
const hash =
|
||
from === to
|
||
? `#L${from + 1}`
|
||
: `#L${from + 1}-L${to + 1}`;
|
||
const url =
|
||
window.location.pathname + window.location.search + hash;
|
||
window.history.replaceState(null, "", url);
|
||
highlightLines(from, to);
|
||
e.stop();
|
||
});
|
||
|
||
window.addEventListener("hashchange", () => applyHashFromUrl(false));
|
||
|
||
_editor.setFontSize($scope.aceOption.fontSize);
|
||
_editor.setReadOnly($scope.aceOption.readOnly);
|
||
_editor.setKeyboardHandler($scope.aceOption.keyBinding);
|
||
_editor.setSelectionStyle(
|
||
$scope.aceOption.fullLineSelection ? "line" : "text"
|
||
);
|
||
_editor.setOption("displayIndentGuides", true);
|
||
_editor.setHighlightActiveLine(
|
||
$scope.aceOption.highlightActiveLine
|
||
);
|
||
if ($scope.aceOption.cursor == "hide") {
|
||
_editor.renderer.$cursorLayer.element.style.display = "none";
|
||
}
|
||
_editor.setHighlightGutterLine(
|
||
$scope.aceOption.highlightGutterLine
|
||
);
|
||
_editor.setShowInvisibles($scope.aceOption.showInvisibles);
|
||
_editor.setDisplayIndentGuides($scope.aceOption.showIndentGuides);
|
||
|
||
_editor.renderer.setShowPrintMargin(
|
||
$scope.aceOption.showPrintMargin
|
||
);
|
||
_editor.setHighlightSelectedWord(
|
||
$scope.aceOption.highlightSelectedWord
|
||
);
|
||
_editor.session.setUseSoftTabs($scope.aceOption.useSoftTab);
|
||
_editor.session.setTabSize($scope.aceOption.tabSize);
|
||
_editor.setBehavioursEnabled($scope.aceOption.enableBehaviours);
|
||
_editor.setFadeFoldWidgets($scope.aceOption.fadeFoldWidgets);
|
||
},
|
||
};
|
||
$scope.$on("dark-mode", (event, on) => {
|
||
if (on) {
|
||
$scope.aceOption.theme = "nord_dark";
|
||
} else {
|
||
$scope.aceOption.theme = "chrome";
|
||
}
|
||
});
|
||
if ($scope.isDarkMode) {
|
||
$scope.aceOption.theme = "nord_dark";
|
||
}
|
||
$scope.type = getType(extension);
|
||
|
||
getContent($scope.filePath, $scope.file);
|
||
}
|
||
|
||
function init() {
|
||
$scope.repoId = $routeParams.repoId;
|
||
$scope.type = "loading";
|
||
$scope.filePath = $routeParams.path || "";
|
||
$scope.paths = $scope.filePath.split("/");
|
||
|
||
getOptions(function (options) {
|
||
fetchFileCounts();
|
||
var chain = $q.resolve();
|
||
for (let i = 0; i < $scope.paths.length; i++) {
|
||
const path = i > 0 ? $scope.paths.slice(0, i).join("/") : "";
|
||
chain = chain.then(function () {
|
||
return $scope.getFiles(path);
|
||
}).then(function () {
|
||
if ($scope.type === "error") {
|
||
return $q.reject("error");
|
||
}
|
||
});
|
||
}
|
||
chain.then(function () {
|
||
if ($scope.files.length == 1 && $scope.files[0].name == "") {
|
||
$scope.files = [];
|
||
$scope.type = "empty";
|
||
} else {
|
||
selectFile();
|
||
updateContent();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
init();
|
||
},
|
||
])
|
||
// anonymizePullRequestController removed - unified into anonymizeController
|
||
.controller("pullRequestController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
"$routeParams",
|
||
"$sce",
|
||
function ($scope, $http, $location, $routeParams, $sce) {
|
||
async function getOption(callback) {
|
||
$http.get(`/api/pr/${$scope.pullRequestId}/options`).then(
|
||
(res) => {
|
||
$scope.options = res.data;
|
||
if ($scope.options.url) {
|
||
// the repository is expired with redirect option
|
||
window.location = $scope.options.url;
|
||
return;
|
||
}
|
||
if (callback) {
|
||
callback(res.data);
|
||
}
|
||
},
|
||
(err) => {
|
||
$scope.type = "error";
|
||
$scope.content = err.data.error;
|
||
}
|
||
);
|
||
}
|
||
async function getPullRequest(callback) {
|
||
$http.get(`/api/pr/${$scope.pullRequestId}/content`).then(
|
||
(res) => {
|
||
$scope.details = res.data;
|
||
if (callback) {
|
||
callback(res.data);
|
||
}
|
||
},
|
||
(err) => {
|
||
$scope.type = "error";
|
||
$scope.content = err.data.error;
|
||
}
|
||
);
|
||
}
|
||
|
||
function init() {
|
||
$scope.pullRequestId = $routeParams.pullRequestId;
|
||
$scope.type = "loading";
|
||
|
||
getOption((_) => {
|
||
getPullRequest();
|
||
});
|
||
}
|
||
|
||
init();
|
||
},
|
||
])
|
||
.controller("gistController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
"$routeParams",
|
||
"$sce",
|
||
function ($scope, $http, $location, $routeParams, $sce) {
|
||
async function getOption(callback) {
|
||
$http.get(`/api/gist/${$scope.gistId}/options`).then(
|
||
(res) => {
|
||
$scope.options = res.data;
|
||
if ($scope.options.url) {
|
||
window.location = $scope.options.url;
|
||
return;
|
||
}
|
||
if (callback) callback(res.data);
|
||
},
|
||
(err) => {
|
||
$scope.type = "error";
|
||
$scope.content = err.data.error;
|
||
}
|
||
);
|
||
}
|
||
async function getGist(callback) {
|
||
$http.get(`/api/gist/${$scope.gistId}/content`).then(
|
||
(res) => {
|
||
$scope.details = res.data;
|
||
if (callback) callback(res.data);
|
||
},
|
||
(err) => {
|
||
$scope.type = "error";
|
||
$scope.content = err.data.error;
|
||
}
|
||
);
|
||
}
|
||
|
||
function init() {
|
||
$scope.gistId = $routeParams.gistId;
|
||
$scope.type = "loading";
|
||
getOption(() => { getGist(); });
|
||
}
|
||
|
||
init();
|
||
},
|
||
])
|
||
.controller("conferencesController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
function ($scope, $http, $location) {
|
||
$scope.$watch("user.status", () => {
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
});
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
|
||
$scope.conferences = [];
|
||
$scope.search = "";
|
||
|
||
const conferencesPrefsKey = "conferences.filterPrefs";
|
||
const conferencesPrefDefaults = {
|
||
filters: { status: { ready: true, expired: false, removed: false } },
|
||
orderBy: "name",
|
||
};
|
||
const savedConferencesPrefs = loadFilterPrefs(conferencesPrefsKey) || {};
|
||
$scope.filters = {
|
||
status: Object.assign(
|
||
{},
|
||
conferencesPrefDefaults.filters.status,
|
||
(savedConferencesPrefs.filters && savedConferencesPrefs.filters.status) || {}
|
||
),
|
||
};
|
||
$scope.orderBy = savedConferencesPrefs.orderBy || conferencesPrefDefaults.orderBy;
|
||
|
||
$scope.$watch("orderBy", () => {
|
||
saveFilterPrefs(conferencesPrefsKey, {
|
||
filters: $scope.filters,
|
||
orderBy: $scope.orderBy,
|
||
});
|
||
});
|
||
$scope.$watch(
|
||
"filters",
|
||
() => {
|
||
saveFilterPrefs(conferencesPrefsKey, {
|
||
filters: $scope.filters,
|
||
orderBy: $scope.orderBy,
|
||
});
|
||
},
|
||
true
|
||
);
|
||
|
||
$scope.removeConference = function (conf) {
|
||
if (
|
||
confirm(
|
||
`Are you sure that you want to remove the conference ${conf.name}? All the repositories linked to this conference will expire.`
|
||
)
|
||
) {
|
||
const toast = {
|
||
title: `Removing ${conf.name}...`,
|
||
date: new Date(),
|
||
body: `The conference ${conf.name} is going to be removed.`,
|
||
};
|
||
$scope.addToast(toast);
|
||
$http.delete(`/api/conferences/${conf.conferenceID}`).then(() => {
|
||
toast.title = `${conf.name} is removed.`;
|
||
toast.body = `The conference ${conf.name} is removed.`;
|
||
getConferences();
|
||
});
|
||
}
|
||
};
|
||
|
||
function getConferences() {
|
||
$http.get("/api/conferences/").then(
|
||
(res) => {
|
||
$scope.conferences = res.data || [];
|
||
},
|
||
(err) => {
|
||
console.error(err);
|
||
}
|
||
);
|
||
}
|
||
getConferences();
|
||
|
||
$scope.conferenceFilter = (conference) => {
|
||
if ($scope.filters.status[conference.status] == false) return false;
|
||
|
||
if ($scope.search.trim().length == 0) return true;
|
||
|
||
if (conference.name.indexOf($scope.search) > -1) return true;
|
||
if (conference.conferenceID.indexOf($scope.search) > -1) return true;
|
||
|
||
return false;
|
||
};
|
||
},
|
||
])
|
||
.controller("newConferenceController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
"$routeParams",
|
||
function ($scope, $http, $location, $routeParams) {
|
||
$scope.$watch("user.status", () => {
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
});
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
|
||
$scope.plans = [];
|
||
$scope.editionMode = false;
|
||
|
||
function getConference() {
|
||
$http
|
||
.get("/api/conferences/" + $routeParams.conferenceId)
|
||
.then((res) => {
|
||
$scope.options = res.data;
|
||
$scope.options.startDate = new Date($scope.options.startDate);
|
||
$scope.options.endDate = new Date($scope.options.endDate);
|
||
});
|
||
}
|
||
if ($routeParams.conferenceId) {
|
||
$scope.editionMode = true;
|
||
getConference();
|
||
}
|
||
|
||
function getPlans() {
|
||
$http.get("/api/conferences/plans").then((res) => {
|
||
$scope.plans = res.data;
|
||
|
||
$scope.plan = $scope.plans.filter(
|
||
(f) => f.id == $scope.options.plan.planID
|
||
)[0];
|
||
});
|
||
}
|
||
getPlans();
|
||
const start = new Date();
|
||
start.setDate(1);
|
||
start.setMonth(start.getMonth() + 1);
|
||
const end = new Date();
|
||
end.setMonth(start.getMonth() + 7, 0);
|
||
$scope.options = {
|
||
startDate: start,
|
||
endDate: end,
|
||
plan: {
|
||
planID: "free_conference",
|
||
},
|
||
options: {
|
||
link: true,
|
||
image: true,
|
||
pdf: true,
|
||
notebook: true,
|
||
update: true,
|
||
page: true,
|
||
},
|
||
};
|
||
$scope.plan = null;
|
||
|
||
$scope.$watch("options.plan.planID", () => {
|
||
$scope.plan = $scope.plans.filter(
|
||
(f) => f.id == $scope.options.plan.planID
|
||
)[0];
|
||
});
|
||
|
||
function resetValidity() {
|
||
$scope.conference.name.$setValidity("required", true);
|
||
$scope.conference.conferenceID.$setValidity("pattern", true);
|
||
$scope.conference.conferenceID.$setValidity("required", true);
|
||
$scope.conference.conferenceID.$setValidity("used", true);
|
||
$scope.conference.startDate.$setValidity("required", true);
|
||
$scope.conference.startDate.$setValidity("invalid", true);
|
||
$scope.conference.endDate.$setValidity("required", true);
|
||
$scope.conference.endDate.$setValidity("invalid", true);
|
||
$scope.conference.$setValidity("error", true);
|
||
}
|
||
|
||
function displayErrorMessage(message) {
|
||
switch (message) {
|
||
case "conf_name_missing":
|
||
$scope.conference.name.$setValidity("required", false);
|
||
break;
|
||
case "conf_id_missing":
|
||
$scope.conference.conferenceID.$setValidity("required", false);
|
||
break;
|
||
case "conf_id_format":
|
||
$scope.conference.conferenceID.$setValidity("pattern", false);
|
||
break;
|
||
case "conf_id_used":
|
||
$scope.conference.conferenceID.$setValidity("used", false);
|
||
break;
|
||
case "conf_start_date_missing":
|
||
$scope.conference.startDate.$setValidity("required", false);
|
||
break;
|
||
case "conf_end_date_missing":
|
||
$scope.conference.endDate.$setValidity("required", false);
|
||
break;
|
||
case "conf_start_date_invalid":
|
||
$scope.conference.startDate.$setValidity("invalid", false);
|
||
break;
|
||
case "conf_end_date_invalid":
|
||
$scope.conference.endDate.$setValidity("invalid", false);
|
||
break;
|
||
default:
|
||
$scope.conference.$setValidity("error", false);
|
||
break;
|
||
}
|
||
}
|
||
|
||
$scope.submit = function () {
|
||
const toast = {
|
||
title: `Creating ${$scope.options.name}...`,
|
||
date: new Date(),
|
||
body: `The conference ${$scope.options.conferenceID} is in creation.`,
|
||
};
|
||
if ($scope.editionMode) {
|
||
toast.title = `Updating ${$scope.options.name}...`;
|
||
toast.body = `The conference '${$scope.options.conferenceID}' is updating.`;
|
||
}
|
||
$scope.addToast(toast);
|
||
resetValidity();
|
||
$http
|
||
.post(
|
||
"/api/conferences/" +
|
||
($scope.editionMode ? $scope.options.conferenceID : ""),
|
||
$scope.options
|
||
)
|
||
.then(
|
||
() => {
|
||
if (!$scope.editionMode) {
|
||
toast.title = `${$scope.options.name} created`;
|
||
toast.body = `The conference '${$scope.options.conferenceID}' is created.`;
|
||
} else {
|
||
toast.title = `${$scope.options.name} updated`;
|
||
toast.body = `The conference '${$scope.options.conferenceID}' is updated.`;
|
||
}
|
||
$location.url("/conference/" + $scope.options.conferenceID);
|
||
},
|
||
(error) => {
|
||
displayErrorMessage(error.data.error);
|
||
$scope.removeToast(toast);
|
||
}
|
||
);
|
||
};
|
||
},
|
||
])
|
||
.controller("conferenceController", [
|
||
"$scope",
|
||
"$http",
|
||
"$location",
|
||
"$routeParams",
|
||
function ($scope, $http, $location, $routeParams) {
|
||
$scope.$watch("user.status", () => {
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
});
|
||
if ($scope.user == null) {
|
||
$location.url("/");
|
||
}
|
||
$scope.conference = null;
|
||
|
||
$scope.search = "";
|
||
$scope.filters = {
|
||
status: { ready: true, expired: false, removed: false },
|
||
};
|
||
$scope.orderBy = "-anonymizeDate";
|
||
|
||
$scope.repoFiler = (repo) => {
|
||
if ($scope.filters.status[repo.status] == false) return false;
|
||
|
||
if ($scope.search.trim().length == 0) return true;
|
||
|
||
if (repo.source.fullName.indexOf($scope.search) > -1) return true;
|
||
if (repo.repoId.indexOf($scope.search) > -1) return true;
|
||
|
||
return false;
|
||
};
|
||
|
||
function getConference() {
|
||
$http
|
||
.get("/api/conferences/" + $routeParams.conferenceId)
|
||
.then((res) => {
|
||
$scope.conference = res.data;
|
||
});
|
||
}
|
||
getConference();
|
||
},
|
||
]);
|
||
|
||
$(document).on("click", "#navbarSupportedContent .nav-link", function (e) {
|
||
if ($(this).attr("data-toggle") === "dropdown") return;
|
||
var $collapse = $("#navbarSupportedContent");
|
||
if ($collapse.hasClass("show")) {
|
||
$collapse.collapse("hide");
|
||
}
|
||
});
|