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/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("diff", [ "$sce", function ($sce) { const esc = (s) => s.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('
| ' + (line.oldNo || "") + " | " + '' + (line.newNo || "") + " | " + '' + (line.kind === "add" ? "+" : line.kind === "remove" ? "-" : line.kind === "hunk" ? "@" : "") + " | " + '' + esc(line.text) + " | " + "
.
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:
' ' +
'
',
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 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: "@" },
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 } };
for (let file of arr) {
let current = keys[file.path].child;
let fPath = `${file.path}/${file.name}`;
if (fPath.startsWith("/")) {
fPath = fPath.substring(1);
}
if (file.size != null) {
// it is a file
current.push({
name: file.name,
size: file.size,
sha: file.sha,
});
} else {
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 - f2.name;
}
if (f1d) {
return -1;
}
if (f2d) {
return 1;
}
return f1.name - f2.name;
};
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 generate(current, parentPath) {
if (!current) return "";
current = current.sort(sortFiles);
const afiles = current;
let output = "";
for (let f of afiles) {
let dir = !!f.child;
let name = f.name;
let size = f.size;
if (dir) {
let test = name;
current = f.child;
while (current && current.length == 1) {
test += "/" + current[0].name;
size = current[0].size;
current = current[0].child;
}
name = test;
if (size != null && size >= 0) {
dir = false;
}
}
if (size != null) {
size = `Size: ${humanFileSize(size || 0)}`;
} else {
size = "";
}
const path = `${parentPath}/${name}`;
const cssClasses = ["file"];
if (dir) {
cssClasses.push("folder");
}
if ($scope.opens[path]) {
cssClasses.push("open");
}
if ($scope.isActive(path)) {
cssClasses.push("active");
}
const truncated = dir && isTruncated(path);
if (truncated) {
cssClasses.push("truncated");
}
output += `- `;
if (dir) {
output += `${name}`;
} else {
output += `${name}`;
}
if (truncated) {
output += ``;
}
if ($scope.opens[path] && f.child) {
if (f.child.length > 1) {
output += generate(f.child, path);
} else if (dir) {
current = f.child;
while (current && current.length == 1) {
current = current[0].child;
}
output += generate(current, path);
}
}
// output += generate(f.child, parentPath + "/" + f.name);
output + "
";
}
return output + "
";
}
function display() {
$element.html("");
const output = generate(toArray($scope.file).sort(sortFiles), "");
$compile(output)($scope, (clone) => {
$element.append(clone);
});
}
// #496 — expand folders whose children are already loaded so
// reviewers see the whole tree without clicking through. Skip
// folders with empty children to avoid emitting an empty
// that breaks the click-time lazy-load (#496-followup): the
// openFolder handler used to detect "needs to load" by looking
// at the absence of a sibling node, but a pre-expanded empty
// is a non-null sibling and silently suppressed the fetch.
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.isActive = function (name) {
return $routeParams.path == name.substring(1);
};
$scope.openFolder = async function (folder, event) {
$scope.opens[folder] = !$scope.opens[folder];
const sib = event.srcElement.nextSibling;
// Lazy-load when there's no sibling (folder never expanded) or
// when the sibling is an empty from a pre-expanded folder
// whose children weren't fetched yet (#496-followup).
const needsLoad =
sib == null ||
(sib.tagName === "UL" && sib.children.length === 0);
if (needsLoad) {
await $scope.$parent.getFiles(folder.substring(1));
$scope.$apply();
}
};
},
],
};
},
])
.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:
"",
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();
}
$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.getStatus = () => {
$http
.get("/api/repo/" + $scope.repoId, {
repoId: $scope.repoId,
repoUrl: $scope.repoUrl,
})
.then(
(res) => {
$scope.repo = res.data;
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;
}
if (
$scope.repo.status != "ready" &&
$scope.repo.status != "error"
) {
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) {
// Preserve the saved commit when editing with auto-update off:
// refreshing branches must not silently bump the pinned SHA to
// GitHub HEAD. Same intent as the source.branch watcher (#360),
// extended to cover the branches refresh path.
const keepSavedCommit =
$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
// 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 '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("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 "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",
"PDFViewerService",
function ($scope, $http, $location, $routeParams, $sce, PDFViewerService) {
$scope.files = [];
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 + readmeCandidates[best_match]);
}
}
$scope.getFiles = async function (path) {
try {
const res = await $http.get(
`/api/repo/${$scope.repoId}/files/?path=${encodeURIComponent(path)}&v=${$scope.options.lastUpdateDate}`
);
const normalized = path || "";
$scope.files = $scope.files.filter((f) => f.path !== normalized);
$scope.files.push(...res.data);
return res.data;
} catch (err) {
$scope.type = "error";
$scope.content = (err && err.data && err.data.error) || "unknown_error";
$scope.files = [];
}
};
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];
}
function getOptions(callback) {
$http.get(`/api/repo/${$scope.repoId}/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;
}
);
}
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/${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/${$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 and
// shift-clicking extends to #L-L 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(async (options) => {
for (let i = 0; i < $scope.paths.length; i++) {
const path = i > 0 ? $scope.paths.slice(0, i).join("/") : "";
await $scope.getFiles(path);
if ($scope.type === "error") {
$scope.$apply();
return;
}
}
if ($scope.files.length == 1 && $scope.files[0].name == "") {
$scope.files = [];
$scope.type = "empty";
$scope.$apply();
} else {
$scope.$apply(() => {
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");
}
});