Merge pull request #811 from tdurieux/t3code/migrate-to-vue-3

refactor: migrate the frontend from AngularJS to Vue 3
This commit is contained in:
Thomas Durieux
2026-09-09 13:33:06 +02:00
committed by GitHub
72 changed files with 6271 additions and 8434 deletions
+2
View File
@@ -21,3 +21,5 @@ scripts
.dockerignore .dockerignore
Dockerfile* Dockerfile*
docker-compose*.yml docker-compose*.yml
/tmp
+22
View File
@@ -109,3 +109,25 @@ Anonymous GitHub either downloads the full repository and anonymizes each file,
## License ## License
[GPL-3.0](LICENSE) © [Thomas Durieux](https://durieux.me) [GPL-3.0](LICENSE) © [Thomas Durieux](https://durieux.me)
### Frontend development
The UI uses Vue 3 and Vue Router with the existing Express API. Page setup
functions live in `public/script/app.js` and `admin.js`; the Vue templates are
in `public/partials/`. Gulp compiles the templates and bundles the app with
esbuild, then updates the asset manifest used by Express.
Run `npm run build:ui` after changing a template or frontend script. Use
`npm run dev:ui` to serve the built UI locally with API requests proxied to the
configured upstream. `npm run test:ui` rebuilds the assets and runs the frontend
regression and DOM interaction tests. `npm run build` also builds the UI for
production.
The initial bundle contains the Vue app. Markdown extensions load on content
routes; PDF.js, Ace, and notebook support load when their viewers mount.
Org support loads in the repository explorer, and Mermaid loads only when a
diagram is encountered. These libraries use hashed URLs and load once per tab.
The Docker build compiles the same bundles and copies them, the asset manifest,
and document-worker assets into the runtime image. The Compose app serves these
assets directly; no frontend development server is needed.
+35 -18
View File
@@ -6,13 +6,11 @@ const { pipeline } = require("node:stream");
const cleanCss = require("gulp-clean-css"); const cleanCss = require("gulp-clean-css");
const crypto = require("crypto"); const crypto = require("crypto");
const fs = require("fs"); const fs = require("fs");
const esbuild = require("esbuild");
const { compileTemplate } = require("vue/compiler-sfc");
const { promisify } = require("node:util");
const coreJsFiles = [ const coreJsFiles = [
"public/script/external/angular.min.js",
"public/script/external/angular-translate.min.js",
"public/script/external/angular-translate-loader-static-files.min.js",
"public/script/external/angular-sanitize.min.js",
"public/script/external/angular-route.min.js",
"public/script/external/github-emojis.js", "public/script/external/github-emojis.js",
"public/script/external/marked-emoji.js", "public/script/external/marked-emoji.js",
"public/script/external/marked.min.js", "public/script/external/marked.min.js",
@@ -25,21 +23,17 @@ const coreJsFiles = [
"public/script/utils.js", "public/script/utils.js",
]; ];
const vendorJsFiles = [ const markdownFiles = [
"public/script/external/pdf.js",
"public/script/pdf-viewer.js",
"public/script/html-doc.js",
"public/script/external/katex.min.js", "public/script/external/katex.min.js",
"public/script/external/katex-auto-render.min.js", "public/script/external/katex-auto-render.min.js",
"public/script/external/marked-katex-extension.umd.min.js", "public/script/external/marked-katex-extension.umd.min.js",
"public/script/external/marked-mermaid.js", "public/script/external/marked-mermaid.js",
"public/script/external/notebook.min.js", ];
"public/script/external/org.js", const pdfFiles = ["public/script/external/pdf.js"];
"public/script/external/ace/ace.js", const notebookFiles = ["public/script/external/notebook.min.js"];
"public/script/external/ui-ace.min.js", const orgFiles = ["public/script/external/org.js"];
"public/script/app.js", const editorFiles = ["public/script/external/ace/ace.js"];
"public/script/admin.js", const lazyGroups = { markdown: markdownFiles, pdf: pdfFiles, notebook: notebookFiles, org: orgFiles, editor: editorFiles };
];
const mermaidFiles = [ const mermaidFiles = [
"public/script/external/mermaid.min.js", "public/script/external/mermaid.min.js",
@@ -70,8 +64,30 @@ function buildCoreJs(cb) {
pipeline(orderedSrc(coreJsFiles), concat("core.min.js"), uglify(), dest("public/script"), cb); pipeline(orderedSrc(coreJsFiles), concat("core.min.js"), uglify(), dest("public/script"), cb);
} }
function buildVendorJs(cb) { async function buildVendorJs() {
pipeline(orderedSrc(vendorJsFiles), concat("vendor.min.js"), uglify(), dest("public/script"), cb); const lazyAssets = {};
await Promise.all(Object.entries(lazyGroups).map(async ([name, files]) => {
await promisify(pipeline)(orderedSrc(files), concat(`${name}.min.js`), uglify(), dest("public/script"));
lazyAssets[name] = `/script/${name}.${hashFile(`public/script/${name}.min.js`)}.min.js`;
}));
const app = await esbuild.build({
entryPoints: ["public/script/main.js"], bundle: true, write: false,
format: "iife", minify: true, target: "es2020",
define: { __LAZY_ASSETS__: JSON.stringify(lazyAssets), "process.env.NODE_ENV": JSON.stringify("production"), __VUE_OPTIONS_API__: "true", __VUE_PROD_DEVTOOLS__: "false", __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false" },
plugins: [{ name: "vue-templates", setup(build) {
build.onLoad({ filter: /\.htm$/ }, async ({ path }) => {
const { code, errors } = compileTemplate({
source: fs.readFileSync(path, "utf8"), filename: path, id: "anonymous",
compilerOptions: { nodeTransforms: [node => {
if (node.type === 1 && node.props.some(prop => prop.type === 7 && ["text", "html"].includes(prop.name))) node.children = [];
}] },
});
if (errors.length) throw errors[0];
return { contents: code, loader: "js" };
});
} }],
});
fs.writeFileSync("public/script/vendor.min.js", app.outputFiles[0].text);
} }
function buildMermaidJs(cb) { function buildMermaidJs(cb) {
@@ -89,6 +105,7 @@ function writeManifest(cb) {
"mermaid.min.js": "public/script/mermaid.min.js", "mermaid.min.js": "public/script/mermaid.min.js",
"all.min.css": "public/css/all.min.css", "all.min.css": "public/css/all.min.css",
}; };
for (const name of Object.keys(lazyGroups)) files[`${name}.min.js`] = `public/script/${name}.min.js`;
const manifest = {}; const manifest = {};
for (const [key, filePath] of Object.entries(files)) { for (const [key, filePath] of Object.entries(files)) {
const hash = hashFile(filePath); const hash = hashFile(filePath);
+1992 -22
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -16,7 +16,9 @@
"build": "rm -rf build && tsc && gulp", "build": "rm -rf build && tsc && gulp",
"knip": "knip", "knip": "knip",
"migrate:credentials": "node -r ts-node/register src/scripts/migrate-credentials.ts", "migrate:credentials": "node -r ts-node/register src/scripts/migrate-credentials.ts",
"recover:owners": "node -r ts-node/register src/scripts/recover-repository-owners.ts" "recover:owners": "node -r ts-node/register src/scripts/recover-repository-owners.ts",
"build:ui": "gulp",
"test:ui": "gulp && mocha test/frontend-regressions.test.js test/dashboard-ui.test.js test/vue-ui.test.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -90,19 +92,23 @@
"@types/unzip-stream": "^0.3.4", "@types/unzip-stream": "^0.3.4",
"c8": "^10.1.3", "c8": "^10.1.3",
"chai": "^4.5.0", "chai": "^4.5.0",
"esbuild": "^0.25.12",
"eslint": "^10.2.0", "eslint": "^10.2.0",
"gulp": "^5.0.0", "gulp": "^5.0.0",
"gulp-clean-css": "^4.3.0", "gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-uglify": "^3.0.2", "gulp-uglify": "^3.0.2",
"http-proxy-middleware": "^3.0.5", "http-proxy-middleware": "^3.0.5",
"jsdom": "^26.1.0",
"knip": "^5.1.0", "knip": "^5.1.0",
"mocha": "^12.0.0", "mocha": "^12.0.0",
"nodemon": "^3.1.0", "nodemon": "^3.1.0",
"ordered-read-streams": "^2.0.0", "ordered-read-streams": "^2.0.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.3", "typescript": "^5.4.3",
"typescript-eslint": "^8.58.2" "typescript-eslint": "^8.58.2",
"vue": "^3.5.42",
"vue-router": "^4.6.4"
}, },
"nodemonConfig": { "nodemonConfig": {
"ignore": [ "ignore": [
+8 -3
View File
@@ -1,6 +1,11 @@
{ {
"core.min.js": "core.6332b3c288.min.js", "core.min.js": "core.c5bd53363a.min.js",
"vendor.min.js": "vendor.21205fda63.min.js", "vendor.min.js": "vendor.af902bc3b4.min.js",
"mermaid.min.js": "mermaid.f848a72d16.min.js", "mermaid.min.js": "mermaid.f848a72d16.min.js",
"all.min.css": "all.08e150389d.min.css" "all.min.css": "all.076c089579.min.css",
"markdown.min.js": "markdown.ad7b1d71c3.min.js",
"pdf.min.js": "pdf.eaa7573247.min.js",
"notebook.min.js": "notebook.8844e2735f.min.js",
"org.min.js": "org.f4e2a3f59f.min.js",
"editor.min.js": "editor.e243722d87.min.js"
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -201,9 +201,10 @@ body {
overflow-y: hidden; overflow-y: hidden;
} }
/* Angular removes the ng-view attribute after compiling the route. Target the /* Keep the route view as the scroll container below the header. */
element so it remains the scroll container below the fixed-height header. */ #app { flex: 1 1 auto; min-height: 0; }
ng-view { .app-header { flex: 0 0 auto; }
.app-view {
display: block; display: block;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
+5 -39
View File
@@ -1,6 +1,6 @@
<!-- index.html --> <!-- index.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" ng-controller="mainController"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta <meta
@@ -8,7 +8,7 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no" content="width=device-width, initial-scale=1, shrink-to-fit=no"
/> />
<title ng-bind="title">Anonymous Github</title> <title>Anonymous Github</title>
<base href="/" /> <base href="/" />
<link <link
@@ -49,38 +49,8 @@
</noscript> </noscript>
<link rel="stylesheet" href="/css/__ALL_CSS__" /> <link rel="stylesheet" href="/css/__ALL_CSS__" />
</head> </head>
<body keypress-events class="d-flex flex-column"> <body class="d-flex flex-column">
<ng-include src="'partials/header.htm'"></ng-include> <div id="app" class="d-flex flex-column"></div>
<ng-view class="align-items-stretch w-100"></ng-view>
<div
class="position-fixed bottom-0 right-0 p-3"
style="z-index: 999999999; right: 0; bottom: 0"
>
<div
id="liveToast"
class="toast show"
role="alert"
aria-live="assertive"
aria-atomic="true"
data-delay="2000"
ng-repeat="toast in toasts"
>
<div class="toast-header">
<strong class="mr-auto" ng-bind="toast.title"></strong>
<button
type="button"
class="ml-2 mb-1 close"
data-dismiss="toast"
aria-label="Close"
ng-click="removeToast(toast);"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body" ng-bind="toast.body"></div>
</div>
</div>
<script src="/script/__CORE_JS__"></script> <script src="/script/__CORE_JS__"></script>
<script> <script>
@@ -103,11 +73,7 @@
document.body.appendChild(s); document.body.appendChild(s);
}; };
</script> </script>
<script src="/script/__VENDOR_JS__" defer onload=" <script src="/script/__VENDOR_JS__" defer></script>
ace.config.set('basePath', '/script/external/ace/');
pdfjsLib.GlobalWorkerOptions.workerSrc = '/script/external/pdf.worker.js';
angular.bootstrap(document, ['anonymous-github']);
"></script>
<script> <script>
window.addEventListener("load", function () { window.addEventListener("load", function () {
var s = document.createElement("script"); var s = document.createElement("script");
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="paper-empty-inner"> <div class="paper-empty-inner">
<div class="paper-eyebrow">Error &middot; 404</div> <div class="paper-eyebrow">Error &middot; 404</div>
<h1 class="paper-empty-title"> <h1 class="paper-empty-title">
This link<br /><em>isn&rsquo;t here.</em> This link<br><em>isn&rsquo;t here.</em>
</h1> </h1>
<p class="paper-empty-lede"> <p class="paper-empty-lede">
The anonymous mirror you&rsquo;re looking for has expired, been removed The anonymous mirror you&rsquo;re looking for has expired, been removed
+42 -46
View File
@@ -12,39 +12,39 @@
</nav> </nav>
<div class="admin-summary"> <div class="admin-summary">
<span class="summary-total">{{total >= 0 ? (total | number) : '…'}}</span> <span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
<span class="summary-pill ok" ng-class="{active: query.ready}" ng-click="query.ready = !query.ready; query.page = 1" title="Toggle ready filter">Ready <span class="count">{{statusCountFor('ready') | number}}</span></span> <span class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></span>
<span class="summary-pill warn" ng-class="{active: query.preparing}" ng-click="query.preparing = !query.preparing; query.page = 1" title="Toggle preparing filter">Preparing <span class="count">{{statusCountFor('preparing') | number}}</span></span> <span class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></span>
<span class="summary-pill error" ng-class="{active: query.error}" ng-click="query.error = !query.error; query.page = 1" title="Toggle errored filter">Errored <span class="count">{{statusCountFor('error') | number}}</span></span> <span class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></span>
<span class="summary-pill" ng-class="{active: query.expired}" ng-click="query.expired = !query.expired; query.page = 1" title="Toggle expired filter">Expired <span class="count">{{statusCountFor('expired') | number}}</span></span> <span class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></span>
<span class="summary-pill" ng-class="{active: query.removed}" ng-click="query.removed = !query.removed; query.page = 1" title="Toggle removed filter">Removed <span class="count">{{statusCountFor('removed') | number}}</span></span> <span class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
</div> </div>
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;"> <div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
<i class="fas fa-exclamation-triangle"></i> {{fetchError}} <i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
</div> </div>
<form class="w-100 admin-filter-toolbar" aria-label="Conferences" accept-charset="UTF-8"> <form class="w-100 admin-filter-toolbar" aria-label="Conferences" accept-charset="UTF-8" v-form="viewState">
<div class="admin-filter-row"> <div class="admin-filter-row">
<div class="search-wrap"> <div class="search-wrap">
<input type="search" class="form-control" placeholder="Search conferences…" autocomplete="off" ng-model="query.search" /> <input type="search" class="form-control" placeholder="Search conferences…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<span class="admin-search-hint" ng-if="!query.search">/</span> <span class="admin-search-hint" v-if="!query?.search">/</span>
</div> </div>
<span class="admin-filter-spacer"></span> <span class="admin-filter-spacer"></span>
<button class="btn btn-sm" type="button" ng-click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button> <button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
<span class="admin-filter-inline" aria-label="Pagination"> <span class="admin-filter-inline" aria-label="Pagination">
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i></button> <button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 1"><i class="fas fa-chevron-left"></i></button>
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{query.page}}/{{totalPage || 1}}</span> <span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{ query?.page }}/{{ totalPage || 1 }}</span>
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage"><i class="fas fa-chevron-right"></i></button> <button class="btn btn-sm" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page &gt;= totalPage"><i class="fas fa-chevron-right"></i></button>
</span> </span>
</div> </div>
<div class="admin-filter-row" ng-if="chips.length"> <div class="admin-filter-row" v-if="chips?.length">
<div class="admin-active-chips"> <div class="admin-active-chips">
<span class="admin-active-chip" ng-repeat="chip in chips track by chip.key"> <span class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
<span class="key">{{chip.label}}</span> <span class="key">{{ chip?.label }}</span>
<span>{{chip.value}}</span> <span>{{ chip?.value }}</span>
<button type="button" ng-click="clearFilter(chip.key)"><i class="fas fa-times"></i></button> <button type="button" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
</span> </span>
</div> </div>
</div> </div>
@@ -52,66 +52,62 @@
<div class="paper-table paper-table-conferences w-100" role="table" aria-label="Conferences"> <div class="paper-table paper-table-conferences w-100" role="table" aria-label="Conferences">
<div class="paper-table-head" role="row"> <div class="paper-table-head" role="row">
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'name'}" ng-click="sortBy('name')">Conference <i class="fas" ng-class="sortIcon('name')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;name&#x27;}" @click="sortBy(&#x27;name&#x27;)">Conference <i class="fas" :class="sortIcon(&#x27;name&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'status'}" ng-click="sortBy('status')">Status <i class="fas" ng-class="sortIcon('status')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></i></span></div>
<div role="columnheader" class="num">Repos</div> <div role="columnheader" class="num">Repos</div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'startDate'}" ng-click="sortBy('startDate')">Window <i class="fas" ng-class="sortIcon('startDate')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;startDate&#x27;}" @click="sortBy(&#x27;startDate&#x27;)">Window <i class="fas" :class="sortIcon(&#x27;startDate&#x27;)"></i></span></div>
<div role="columnheader" aria-label="Actions"></div> <div role="columnheader" aria-label="Actions"></div>
</div> </div>
<div <div class="paper-table-row" role="row" v-for="(conference, index) in filteredConferences">
class="paper-table-row"
role="row"
ng-repeat="conference in conferences as filteredConferences"
>
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<span class="type-badge type-repo">Conf</span> <span class="type-badge type-repo">Conf</span>
<div class="anon-text"> <div class="anon-text">
<a class="repo-name" ng-href="/conference/{{conference.conferenceID}}" ng-bind="conference.name"></a> <a class="repo-name" v-text="conference?.name" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID))"></a>
<div class="anon-sub"> <div class="anon-sub">
<span>{{conference.conferenceID}}</span>&nbsp;&middot;&nbsp;<span>{{conference.price || 0 | number}} &euro;</span> <span>{{ conference?.conferenceID }}</span>&nbsp;&middot;&nbsp;<span>{{ fmt?.number(conference?.price || 0) }} &euro;</span>
</div> </div>
</div> </div>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<span class="status-dot" ng-class="{'status-removed': conference.status == 'removed' || conference.status == 'expired', 'status-ready': conference.status == 'ready', 'status-error': conference.status == 'error', 'status-preparing': conference.status == 'preparing'}"></span> <span class="status-dot" :class="{&#x27;status-removed&#x27;: conference?.status == &#x27;removed&#x27; || conference?.status == &#x27;expired&#x27;, &#x27;status-ready&#x27;: conference?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: conference?.status == &#x27;error&#x27;, &#x27;status-preparing&#x27;: conference?.status == &#x27;preparing&#x27;}"></span>
<span ng-bind="conference.status | title"></span> <span v-text="fmt?.title(conference?.status)"></span>
</div> </div>
<div class="cell-views num" role="cell"> <div class="cell-views num" role="cell">
<a ng-href="/admin/?conference={{conference.conferenceID}}" ng-bind="::conference.repositories.length || 0 | number" title="Show repositories in this conference"></a> <a title="Show repositories in this conference" v-text="fmt?.number(conference?.repositories?.length || 0)" :href="safeUrl(&quot;/admin/?conference=&quot; + (conference?.conferenceID))"></a>
</div> </div>
<div class="cell-expires" role="cell">{{conference.startDate | date}} &ndash; {{conference.endDate | date}}</div> <div class="cell-expires" role="cell">{{ fmt?.date(conference?.startDate) }} &ndash; {{ fmt?.date(conference?.endDate) }}</div>
<div class="cell-actions" role="cell"> <div class="cell-actions" role="cell">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions"> <button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
<i class="fas fa-ellipsis-h" aria-hidden="true"></i> <i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/conference/{{conference.conferenceID}}/edit"><i class="far fa-edit"></i> Edit</a> <a class="dropdown-item" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/edit&quot;)"><i class="far fa-edit"></i> Edit</a>
<a class="dropdown-item" href="/conference/{{conference.conferenceID}}/"><i class="fa fa-eye"></i> View</a> <a class="dropdown-item" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/&quot;)"><i class="fa fa-eye"></i> View</a>
<a class="dropdown-item" href="/admin/?conference={{conference.conferenceID}}"><i class="fas fa-code-branch"></i> View repositories</a> <a class="dropdown-item" :href="safeUrl(&quot;/admin/?conference=&quot; + (conference?.conferenceID))"><i class="fas fa-code-branch"></i> View repositories</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#" ng-show="conference.status != 'removed'" ng-click="removeConference(conference)"><i class="fas fa-trash-alt"></i> Remove</a> <a class="dropdown-item text-danger" href="#" v-show="conference?.status != &#x27;removed&#x27;" @click.prevent="removeConference(conference)"><i class="fas fa-trash-alt"></i> Remove</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="paper-table-empty" ng-if="filteredConferences.length == 0"> <div class="paper-table-empty" v-if="filteredConferences?.length == 0">
<i class="fas fa-inbox"></i> <i class="fas fa-inbox"></i>
<span>No conferences match the current filters.</span> <span>No conferences match the current filters.</span>
</div> </div>
</div> </div>
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;"> <div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
<span style="font-size: 12px; color: var(--ink-muted);">{{total | number}} results</span> <span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
<div class="pagination-compact" ng-if="totalPage > 1"> <div class="pagination-compact" v-if="totalPage &gt; 1">
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button> <button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 1"><i class="fas fa-chevron-left"></i> Previous</button>
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" style="width: 56px;" /> <input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value =&gt; { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
<span>of {{totalPage}}</span> <span>of {{ totalPage }}</span>
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">Next <i class="fas fa-chevron-right"></i></button> <button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page &gt;= totalPage">Next <i class="fas fa-chevron-right"></i></button>
</div> </div>
<span class="admin-filter-inline"> <span class="admin-filter-inline">
<label>Per page</label> <label>Per page</label>
<select class="form-control form-control-sm" ng-model="query.limit"><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100">100</option><option value="250">250</option></select> <select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.limit = value }, value: query?.limit, form: null, options: {} }"><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100">100</option><option value="250">250</option></select>
</span> </span>
</div> </div>
</div> </div>
+71 -71
View File
@@ -4,8 +4,8 @@
<header class="errors-header"> <header class="errors-header">
<h1 class="paper-page-title">Errors</h1> <h1 class="paper-page-title">Errors</h1>
<div class="errors-actions"> <div class="errors-actions">
<button class="btn btn-sm" type="button" ng-click="exportCsv()"><i class="fas fa-file-export"></i> Export CSV</button> <button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-export"></i> Export CSV</button>
<button class="btn btn-sm btn-danger" type="button" ng-click="clearAll()"><i class="fas fa-trash"></i> Clear all</button> <button class="btn btn-sm btn-danger" type="button" @click="clearAll()"><i class="fas fa-trash"></i> Clear all</button>
</div> </div>
</header> </header>
@@ -21,33 +21,33 @@
<section class="kpi-grid"> <section class="kpi-grid">
<div class="kpi-card"> <div class="kpi-card">
<div class="kpi-label">Last 24h</div> <div class="kpi-label">Last 24h</div>
<div class="kpi-value">{{stats.last24h}}</div> <div class="kpi-value">{{ stats?.last24h }}</div>
<div class="kpi-sub" ng-class="{up: stats.delta > 0, down: stats.delta < 0}"> <div class="kpi-sub" :class="{up: stats?.delta &gt; 0, down: stats?.delta &lt; 0}">
<span ng-if="stats.prev24h">{{stats.delta > 0 ? '+' : ''}}{{stats.delta}}% vs yesterday</span> <span v-if="stats?.prev24h">{{ stats?.delta > 0 ? '+' : '' }}{{ stats?.delta }}% vs yesterday</span>
<span ng-if="!stats.prev24h">no prior baseline</span> <span v-if="!stats?.prev24h">no prior baseline</span>
</div> </div>
</div> </div>
<div class="kpi-card kpi-error"> <div class="kpi-card kpi-error">
<div class="kpi-label">Errors (5xx)</div> <div class="kpi-label">Errors (5xx)</div>
<div class="kpi-value">{{stats.severity.error}}</div> <div class="kpi-value">{{ stats?.severity?.error }}</div>
<div class="kpi-sub">{{stats.unique.error}} unique</div> <div class="kpi-sub">{{ stats?.unique?.error }} unique</div>
</div> </div>
<div class="kpi-card kpi-warn"> <div class="kpi-card kpi-warn">
<div class="kpi-label">Warnings (4xx)</div> <div class="kpi-label">Warnings (4xx)</div>
<div class="kpi-value">{{stats.severity.warn}}</div> <div class="kpi-value">{{ stats?.severity?.warn }}</div>
<div class="kpi-sub">{{stats.unique.warn}} unique</div> <div class="kpi-sub">{{ stats?.unique?.warn }} unique</div>
</div> </div>
<div class="kpi-card kpi-info"> <div class="kpi-card kpi-info">
<div class="kpi-label">Info (auth, 404)</div> <div class="kpi-label">Info (auth, 404)</div>
<div class="kpi-value">{{stats.severity.info}}</div> <div class="kpi-value">{{ stats?.severity?.info }}</div>
<div class="kpi-sub">{{stats.unique.info}} unique</div> <div class="kpi-sub">{{ stats?.unique?.info }} unique</div>
</div> </div>
<div class="kpi-card" ng-class="{'kpi-error': stats.dropped > 0}"> <div class="kpi-card" :class="{&#x27;kpi-error&#x27;: stats?.dropped &gt; 0}">
<div class="kpi-label">Captured</div> <div class="kpi-label">Captured</div>
<div class="kpi-value">{{total}}</div> <div class="kpi-value">{{ total }}</div>
<div class="kpi-sub"> <div class="kpi-sub">
cap {{cap}} · {{available ? 'live' : 'redis off'}} cap {{ cap }} · {{ available ? 'live' : 'redis off' }}
<span ng-if="stats.dropped > 0" class="dropped-warn"> · {{stats.dropped}} dropped</span> <span class="dropped-warn" v-if="stats?.dropped &gt; 0"> · {{ stats?.dropped }} dropped</span>
</div> </div>
</div> </div>
</section> </section>
@@ -62,55 +62,55 @@
</span> </span>
</div> </div>
<div class="volume-bars"> <div class="volume-bars">
<div class="volume-bar" ng-repeat="b in stats.buckets track by $index" title="{{bucketTitle(b)}}"> <div class="volume-bar" v-for="(b, index) in stats?.buckets" :key="index" :title="(bucketTitle(b))">
<span class="seg seg-error" ng-style="{height: barPx(b, 'error') + 'px'}"></span> <span class="seg seg-error" :style="{height: barPx(b, &#x27;error&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-warn" ng-style="{height: barPx(b, 'warn') + 'px'}"></span> <span class="seg seg-warn" :style="{height: barPx(b, &#x27;warn&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-info" ng-style="{height: barPx(b, 'info') + 'px'}"></span> <span class="seg seg-info" :style="{height: barPx(b, &#x27;info&#x27;) + &#x27;px&#x27;}"></span>
</div> </div>
</div> </div>
</section> </section>
<form class="errors-toolbar" aria-label="Error filters"> <form class="errors-toolbar" aria-label="Error filters" v-form="viewState">
<div class="seg-tabs"> <div class="seg-tabs">
<button type="button" ng-class="{active: query.bucket === ''}" ng-click="setBucket('')">All</button> <button type="button" :class="{active: query?.bucket === &#x27;&#x27;}" @click="setBucket(&#x27;&#x27;)">All</button>
<button type="button" ng-class="{active: query.bucket === 'error'}" ng-click="setBucket('error')">5xx</button> <button type="button" :class="{active: query?.bucket === &#x27;error&#x27;}" @click="setBucket(&#x27;error&#x27;)">5xx</button>
<button type="button" ng-class="{active: query.bucket === 'warn'}" ng-click="setBucket('warn')">4xx</button> <button type="button" :class="{active: query?.bucket === &#x27;warn&#x27;}" @click="setBucket(&#x27;warn&#x27;)">4xx</button>
<button type="button" ng-class="{active: query.bucket === 'info'}" ng-click="setBucket('info')">Info</button> <button type="button" :class="{active: query?.bucket === &#x27;info&#x27;}" @click="setBucket(&#x27;info&#x27;)">Info</button>
</div> </div>
<div class="search-wrap"> <div class="search-wrap">
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
<input type="search" class="form-control" placeholder="code:repo_not_found module:route status:>=400" ng-model="query.search" autocomplete="off" /> <input type="search" class="form-control" placeholder="code:repo_not_found module:route status:&gt;=400" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<span class="filter-count" ng-if="parsedFilterCount">{{parsedFilterCount}} filter{{parsedFilterCount > 1 ? 's' : ''}}</span> <span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
</div> </div>
<div class="select-wrap"> <div class="select-wrap">
<label>Sort</label> <label>Sort</label>
<select class="form-control form-control-sm" ng-model="query.sort"> <select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.sort = value }, value: query?.sort, form: null, options: {} }">
<option value="recent">Most recent</option> <option value="recent">Most recent</option>
<option value="count">Most frequent</option> <option value="count">Most frequent</option>
</select> </select>
</div> </div>
<div class="select-wrap"> <div class="select-wrap">
<label>Group</label> <label>Group</label>
<select class="form-control form-control-sm" ng-model="query.group"> <select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.group = value }, value: query?.group, form: null, options: {} }">
<option value="">Off</option> <option value="">Off</option>
<option value="code">By code</option> <option value="code">By code</option>
<option value="module">By module</option> <option value="module">By module</option>
</select> </select>
</div> </div>
<label class="autoref"> <label class="autoref">
<input type="checkbox" ng-model="query.autoRefresh" /> <input type="checkbox" v-field="{ state: viewState, set: value =&gt; { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
Auto-refresh Auto-refresh
</label> </label>
<button class="btn btn-sm btn-icon" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button> <button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
</form> </form>
<div ng-if="!visible.length" class="admin-empty">No errors captured.</div> <div class="admin-empty" v-if="!visible?.length">No errors captured.</div>
<div ng-if="canLoadMore() && visible.length" class="errors-pager"> <div class="errors-pager" v-if="canLoadMore() &amp;&amp; visible?.length">
<span>Showing {{entries.length}} of {{total}} captured</span> <span>Showing {{ entries?.length }} of {{ total }} captured</span>
<button class="btn btn-sm" type="button" ng-click="loadMore()">Load older</button> <button class="btn btn-sm" type="button" @click="loadMore()">Load older</button>
</div> </div>
<div class="errors-list" ng-if="visible.length"> <div class="errors-list" v-if="visible?.length">
<div class="errors-list-head"> <div class="errors-list-head">
<span class="col-when">When</span> <span class="col-when">When</span>
<span class="col-sev">Severity</span> <span class="col-sev">Severity</span>
@@ -120,75 +120,75 @@
<span class="col-status">Status</span> <span class="col-status">Status</span>
</div> </div>
<div class="errors-row" ng-repeat="row in visible track by row._key" ng-class="{open: expanded[row._key]}"> <div class="errors-row" v-for="(row, index) in visible" :key="row?._key" :class="{open: expanded[row?._key]}">
<div class="errors-row-main" ng-click="toggle(row)"> <div class="errors-row-main" @click="toggle(row)">
<div class="col-when"> <div class="col-when">
<div class="when-rel">{{relTime(row.ts)}}</div> <div class="when-rel">{{ relTime(row?.ts) }}</div>
<div class="when-abs">{{absTimeShort(row.ts)}}</div> <div class="when-abs">{{ absTimeShort(row?.ts) }}</div>
</div> </div>
<div class="col-sev"> <div class="col-sev">
<span class="sev-dot" ng-class="'sev-' + row._bucket"></span> <span class="sev-dot" :class="&#x27;sev-&#x27; + row?._bucket"></span>
<span class="sev-label">{{row._bucket | uppercase}}</span> <span class="sev-label">{{ fmt?.uppercase(row?._bucket) }}</span>
</div> </div>
<div class="col-mod"><span class="pill pill-module">{{row.module}}</span></div> <div class="col-mod"><span class="pill pill-module">{{ row?.module }}</span></div>
<div class="col-msg"> <div class="col-msg">
<strong class="msg-code">{{row.displayMessage}}</strong> <strong class="msg-code">{{ row?.displayMessage }}</strong>
<span class="msg-context" ng-if="row.displayContext && row.displayContext !== row.displayMessage">{{row.displayContext}}</span> <span class="msg-context" v-if="row?.displayContext &amp;&amp; row?.displayContext !== row?.displayMessage">{{ row?.displayContext }}</span>
<span class="msg-detail" ng-if="row._detail">{{row._detail}}</span> <span class="msg-detail" v-if="row?._detail">{{ row?._detail }}</span>
<div class="msg-url" ng-if="row._url">{{row._url}}</div> <div class="msg-url" v-if="row?._url">{{ row?._url }}</div>
</div> </div>
<div class="col-count"> <div class="col-count">
<span class="count-pill" ng-if="row.count > 1">×{{row.count}}</span> <span class="count-pill" v-if="row?.count &gt; 1">×{{ row?.count }}</span>
<span class="count-pill count-pill-muted" ng-if="row.count === 1">×1</span> <span class="count-pill count-pill-muted" v-if="row?.count === 1">×1</span>
</div> </div>
<div class="col-status"> <div class="col-status">
<span class="status-pill" ng-if="row._status" ng-class="'status-' + row._bucket">{{row._status}}</span> <span class="status-pill" v-if="row?._status" :class="&#x27;status-&#x27; + row?._bucket">{{ row?._status }}</span>
</div> </div>
</div> </div>
<div class="errors-row-detail" ng-if="expanded[row._key]"> <div class="errors-row-detail" v-if="expanded[row?._key]">
<div class="detail-tabs"> <div class="detail-tabs">
<button type="button" ng-class="{active: detailTab[row._key] === 'raw' || !detailTab[row._key]}" ng-click="detailTab[row._key] = 'raw'">Raw</button> <button type="button" :class="{active: detailTab[row?._key] === &#x27;raw&#x27; || !detailTab[row?._key]}" @click="detailTab[row._key] = &#x27;raw&#x27;">Raw</button>
<button type="button" ng-class="{active: detailTab[row._key] === 'stack'}" ng-click="detailTab[row._key] = 'stack'" ng-if="row._stack">Stack</button> <button type="button" v-if="row?._stack" :class="{active: detailTab[row?._key] === &#x27;stack&#x27;}" @click="detailTab[row._key] = &#x27;stack&#x27;">Stack</button>
<button type="button" ng-class="{active: detailTab[row._key] === 'related'}" ng-click="detailTab[row._key] = 'related'" ng-if="row.count > 1">Related ({{row.count}})</button> <button type="button" v-if="row?.count &gt; 1" :class="{active: detailTab[row?._key] === &#x27;related&#x27;}" @click="detailTab[row._key] = &#x27;related&#x27;">Related ({{ row?.count }})</button>
</div> </div>
<div class="detail-body"> <div class="detail-body">
<div class="detail-main"> <div class="detail-main">
<pre ng-if="(detailTab[row._key] || 'raw') === 'raw'">{{row._detailJson}}</pre> <pre v-if="(detailTab[row?._key] || &#x27;raw&#x27;) === &#x27;raw&#x27;">{{ row?._detailJson }}</pre>
<pre ng-if="detailTab[row._key] === 'stack'" class="stack-pre">{{row._stack}}</pre> <pre class="stack-pre" v-if="detailTab[row?._key] === &#x27;stack&#x27;">{{ row?._stack }}</pre>
<div ng-if="detailTab[row._key] === 'related'" class="related-list"> <div class="related-list" v-if="detailTab[row?._key] === &#x27;related&#x27;">
<div class="related-row" ng-repeat="r in row._related track by $index"> <div class="related-row" v-for="(r, index) in row?._related" :key="index">
<span class="when-abs">{{absTimeShort(r.ts)}}</span> <span class="when-abs">{{ absTimeShort(r?.ts) }}</span>
<span class="msg-url">{{r._url}}</span> <span class="msg-url">{{ r?._url }}</span>
<span class="status-pill" ng-if="r._status" ng-class="'status-' + r._bucket">{{r._status}}</span> <span class="status-pill" v-if="r?._status" :class="&#x27;status-&#x27; + r?._bucket">{{ r?._status }}</span>
</div> </div>
</div> </div>
<div class="detail-actions"> <div class="detail-actions">
<button class="btn btn-sm" type="button" ng-click="copyCurl(row)" title="Copy a curl that reproduces the request"><i class="fas fa-terminal"></i> Copy curl</button> <button class="btn btn-sm" type="button" title="Copy a curl that reproduces the request" @click="copyCurl(row)"><i class="fas fa-terminal"></i> Copy curl</button>
<button class="btn btn-sm" type="button" ng-click="copyJson(row)"><i class="fas fa-clipboard"></i> Copy JSON</button> <button class="btn btn-sm" type="button" @click="copyJson(row)"><i class="fas fa-clipboard"></i> Copy JSON</button>
<span class="copy-hint" ng-if="copyHint">{{copyHint}}</span> <span class="copy-hint" v-if="copyHint">{{ copyHint }}</span>
</div> </div>
</div> </div>
<aside class="detail-aside"> <aside class="detail-aside">
<div class="aside-block"> <div class="aside-block">
<div class="aside-label">First seen</div> <div class="aside-label">First seen</div>
<div class="aside-value" title="{{absTime(row._firstSeen)}}">{{relTime(row._firstSeen)}}</div> <div class="aside-value" :title="(absTime(row?._firstSeen))">{{ relTime(row?._firstSeen) }}</div>
</div> </div>
<div class="aside-block"> <div class="aside-block">
<div class="aside-label">Last seen</div> <div class="aside-label">Last seen</div>
<div class="aside-value" title="{{absTime(row.ts)}}">{{relTime(row.ts)}}</div> <div class="aside-value" :title="(absTime(row?.ts))">{{ relTime(row?.ts) }}</div>
</div> </div>
<div class="aside-block"> <div class="aside-block">
<div class="aside-label">Occurrences</div> <div class="aside-label">Occurrences</div>
<div class="aside-value">{{row.count}}<span class="aside-sub" ng-if="row._lastHourCount"> · {{row._lastHourCount}} this hour</span></div> <div class="aside-value">{{ row?.count }}<span class="aside-sub" v-if="row?._lastHourCount"> · {{ row?._lastHourCount }} this hour</span></div>
</div> </div>
<div class="aside-block" ng-if="row._repoId"> <div class="aside-block" v-if="row?._repoId">
<div class="aside-label">Repository</div> <div class="aside-label">Repository</div>
<a class="aside-value" ng-href="/r/{{row._repoId}}">{{row._repoId}}</a> <a class="aside-value" :href="safeUrl(&quot;/r/&quot; + (row?._repoId))">{{ row?._repoId }}</a>
</div> </div>
<div class="aside-block" ng-if="row._url"> <div class="aside-block" v-if="row?._url">
<div class="aside-label">URL</div> <div class="aside-label">URL</div>
<div class="aside-value mono">{{row._url}}</div> <div class="aside-value mono">{{ row?._url }}</div>
</div> </div>
</aside> </aside>
</div> </div>
+88 -96
View File
@@ -14,37 +14,37 @@
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a> <a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav> </nav>
<div ng-if="loading" class="admin-empty">Loading overview&hellip;</div> <div class="admin-empty" v-if="loading">Loading overview&hellip;</div>
<div ng-if="error" class="alert alert-danger" style="margin:12px 0"><i class="fas fa-exclamation-triangle"></i> {{error}}</div> <div class="alert alert-danger" style="margin:12px 0" v-if="error"><i class="fas fa-exclamation-triangle"></i> {{ error }}</div>
<div ng-if="data"> <div v-if="data">
<!-- ── Top KPI row ──────────────────────────────────────────── --> <!-- ── Top KPI row ──────────────────────────────────────────── -->
<section class="ov-kpi-row"> <section class="ov-kpi-row">
<div class="ov-kpi-card"> <div class="ov-kpi-card">
<div class="ov-kpi-label">Repositories <span class="ov-dot ov-dot-ok"></span></div> <div class="ov-kpi-label">Repositories <span class="ov-dot ov-dot-ok"></span></div>
<div class="ov-kpi-value">{{humanNum(data.repos.total)}}</div> <div class="ov-kpi-value">{{ humanNum(data?.repos?.total) }}</div>
<div class="ov-kpi-sub">+{{data.repos.newRepos24h}} &middot; last 24h</div> <div class="ov-kpi-sub">+{{ data?.repos?.newRepos24h }} &middot; last 24h</div>
</div> </div>
<div class="ov-kpi-card"> <div class="ov-kpi-card">
<div class="ov-kpi-label">CPU</div> <div class="ov-kpi-label">CPU</div>
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.cpuPercent > 80}">{{data.system.cpuPercent}}%</div> <div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.cpuPercent &gt; 80}">{{ data?.system?.cpuPercent }}%</div>
<div class="ov-kpi-sub">{{data.system.cpuCount}} cores &middot; load {{data.system.loadAvg[0].toFixed(1)}}</div> <div class="ov-kpi-sub">{{ data?.system?.cpuCount }} cores &middot; load {{ data?.system?.loadAvg?.[0]?.toFixed(1) }}</div>
</div> </div>
<div class="ov-kpi-card"> <div class="ov-kpi-card">
<div class="ov-kpi-label">Memory</div> <div class="ov-kpi-label">Memory</div>
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.memPercent > 85}">{{data.system.memPercent}}%</div> <div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.memPercent &gt; 85}">{{ data?.system?.memPercent }}%</div>
<div class="ov-kpi-sub">{{humanBytes(data.system.memUsed)}} / {{humanBytes(data.system.memTotal)}}</div> <div class="ov-kpi-sub">{{ humanBytes(data?.system?.memUsed) }} / {{ humanBytes(data?.system?.memTotal) }}</div>
</div> </div>
<div class="ov-kpi-card"> <div class="ov-kpi-card">
<div class="ov-kpi-label">Disk</div> <div class="ov-kpi-label">Disk</div>
<div class="ov-kpi-value" ng-class="{'ov-val-warn': data.system.diskPercent > 85}">{{data.system.diskPercent}}%</div> <div class="ov-kpi-value" :class="{&#x27;ov-val-warn&#x27;: data?.system?.diskPercent &gt; 85}">{{ data?.system?.diskPercent }}%</div>
<div class="ov-kpi-sub">{{humanBytes(data.system.diskUsed)}} / {{humanBytes(data.system.diskTotal)}} &middot; {{data.system.diskMount}}</div> <div class="ov-kpi-sub">{{ humanBytes(data?.system?.diskUsed) }} / {{ humanBytes(data?.system?.diskTotal) }} &middot; {{ data?.system?.diskMount }}</div>
</div> </div>
<div class="ov-kpi-card"> <div class="ov-kpi-card">
<div class="ov-kpi-label">Uptime</div> <div class="ov-kpi-label">Uptime</div>
<div class="ov-kpi-value">{{humanDuration(data.system.uptime)}}</div> <div class="ov-kpi-value">{{ humanDuration(data?.system?.uptime) }}</div>
<div class="ov-kpi-sub">{{data.system.nodeVersion}} &middot; {{data.system.platform}}</div> <div class="ov-kpi-sub">{{ data?.system?.nodeVersion }} &middot; {{ data?.system?.platform }}</div>
</div> </div>
</section> </section>
@@ -52,17 +52,17 @@
<section class="ov-daily-row"> <section class="ov-daily-row">
<div class="ov-daily-card"> <div class="ov-daily-card">
<div class="ov-daily-label">New repos today</div> <div class="ov-daily-label">New repos today</div>
<div class="ov-daily-value">+{{data.daily.today.repositories | number}}</div> <div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.repositories) }}</div>
<div class="ov-daily-sub">since yesterday</div> <div class="ov-daily-sub">since yesterday</div>
</div> </div>
<div class="ov-daily-card"> <div class="ov-daily-card">
<div class="ov-daily-label">New users today</div> <div class="ov-daily-label">New users today</div>
<div class="ov-daily-value">+{{data.daily.today.users | number}}</div> <div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.users) }}</div>
<div class="ov-daily-sub">{{data.users.total | number}} total users</div> <div class="ov-daily-sub">{{ fmt?.number(data?.users?.total) }} total users</div>
</div> </div>
<div class="ov-daily-card"> <div class="ov-daily-card">
<div class="ov-daily-label">Page views today</div> <div class="ov-daily-label">Page views today</div>
<div class="ov-daily-value">+{{data.daily.today.pageViews | number}}</div> <div class="ov-daily-value">+{{ fmt?.number(data?.daily?.today?.pageViews) }}</div>
<div class="ov-daily-sub">since yesterday</div> <div class="ov-daily-sub">since yesterday</div>
</div> </div>
</section> </section>
@@ -74,17 +74,15 @@
<span class="ov-chart-title">Daily page views &middot; 30d</span> <span class="ov-chart-title">Daily page views &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day</span> <span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day</span>
</div> </div>
<div class="ov-spark-bars" ng-if="data.history.length"> <div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index" <div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyPageViews)) + &quot; views&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyPageViews)) + &quot; views&quot;">
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views" <span class="ov-spark-fill" :style="{height: historyBarH(d, &#x27;dailyPageViews&#x27;) + &#x27;px&#x27;}"></span>
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyPageViews | number}} views">
<span class="ov-spark-fill" ng-style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
</div> </div>
</div> </div>
<div class="ov-spark-x" ng-if="data.history.length"> <div class="ov-spark-x" v-if="data?.history?.length">
<span>{{historyLabel(data.history[0])}}</span> <span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span> <span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span> <span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div> </div>
</div> </div>
<div class="ov-chart-card"> <div class="ov-chart-card">
@@ -92,17 +90,15 @@
<span class="ov-chart-title">New repos &middot; 30d</span> <span class="ov-chart-title">New repos &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day</span> <span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day</span>
</div> </div>
<div class="ov-spark-bars" ng-if="data.history.length"> <div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index" <div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyRepositories)) + &quot; repos&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyRepositories)) + &quot; repos&quot;">
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos" <span class="ov-spark-fill ov-spark-fill-alt" :style="{height: historyBarH(d, &#x27;dailyRepositories&#x27;) + &#x27;px&#x27;}"></span>
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyRepositories | number}} repos">
<span class="ov-spark-fill ov-spark-fill-alt" ng-style="{height: historyBarH(d, 'dailyRepositories') + 'px'}"></span>
</div> </div>
</div> </div>
<div class="ov-spark-x" ng-if="data.history.length"> <div class="ov-spark-x" v-if="data?.history?.length">
<span>{{historyLabel(data.history[0])}}</span> <span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span> <span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span> <span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div> </div>
</div> </div>
<div class="ov-chart-card"> <div class="ov-chart-card">
@@ -110,17 +106,15 @@
<span class="ov-chart-title">Users &middot; 30d</span> <span class="ov-chart-title">Users &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users</span> <span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users</span>
</div> </div>
<div class="ov-spark-bars" ng-if="data.history.length"> <div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index" <div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: &quot; + (fmt?.number(d?.nbUsers)) + &quot; users&quot;" :aria-label="(historyLabel(d)) + &quot;: &quot; + (fmt?.number(d?.nbUsers)) + &quot; users&quot;">
ng-attr-data-tip="{{historyLabel(d)}}: {{d.nbUsers | number}} users" <span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyBarH(d, &#x27;nbUsers&#x27;) + &#x27;px&#x27;}"></span>
ng-attr-aria-label="{{historyLabel(d)}}: {{d.nbUsers | number}} users">
<span class="ov-spark-fill ov-spark-fill-user" ng-style="{height: historyBarH(d, 'nbUsers') + 'px'}"></span>
</div> </div>
</div> </div>
<div class="ov-spark-x" ng-if="data.history.length"> <div class="ov-spark-x" v-if="data?.history?.length">
<span>{{historyLabel(data.history[0])}}</span> <span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span> <span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span> <span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div> </div>
</div> </div>
<div class="ov-chart-card"> <div class="ov-chart-card">
@@ -128,17 +122,15 @@
<span class="ov-chart-title">New users &middot; 30d</span> <span class="ov-chart-title">New users &middot; 30d</span>
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-new-user-fill"></span>users/day</span> <span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-new-user-fill"></span>users/day</span>
</div> </div>
<div class="ov-spark-bars" ng-if="data.history.length"> <div class="ov-spark-bars" v-if="data?.history?.length">
<div class="ov-spark-col has-tip" ng-repeat="d in data.history track by $index" <div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyUsers)) + &quot; users&quot;" :aria-label="(historyLabel(d)) + &quot;: +&quot; + (fmt?.number(d?.dailyUsers)) + &quot; users&quot;">
ng-attr-data-tip="{{historyLabel(d)}}: +{{d.dailyUsers | number}} users" <span class="ov-spark-fill ov-spark-fill-new-user" :style="{height: historyBarH(d, &#x27;dailyUsers&#x27;) + &#x27;px&#x27;}"></span>
ng-attr-aria-label="{{historyLabel(d)}}: +{{d.dailyUsers | number}} users">
<span class="ov-spark-fill ov-spark-fill-new-user" ng-style="{height: historyBarH(d, 'dailyUsers') + 'px'}"></span>
</div> </div>
</div> </div>
<div class="ov-spark-x" ng-if="data.history.length"> <div class="ov-spark-x" v-if="data?.history?.length">
<span>{{historyLabel(data.history[0])}}</span> <span>{{ historyLabel(data?.history[0]) }}</span>
<span>{{historyLabel(data.history[Math.floor(data.history.length/2)])}}</span> <span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
<span>{{historyLabel(data.history[data.history.length - 1])}}</span> <span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
</div> </div>
</div> </div>
</section> </section>
@@ -150,21 +142,21 @@
<div class="ov-panel-card"> <div class="ov-panel-card">
<div class="ov-panel-head"> <div class="ov-panel-head">
<span class="ov-panel-title">Repo status</span> <span class="ov-panel-title">Repo status</span>
<span class="ov-panel-meta">{{data.repos.total | number}} total</span> <span class="ov-panel-meta">{{ fmt?.number(data?.repos?.total) }} total</span>
</div> </div>
<div class="ov-stacked-bar"> <div class="ov-stacked-bar">
<span class="ov-bar-seg ov-bar-ready" ng-style="{width: barPct('ready') + '%'}" title="Ready"></span> <span class="ov-bar-seg ov-bar-ready" title="Ready" :style="{width: barPct(&#x27;ready&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-preparing" ng-style="{width: barPct('preparing') + '%'}" title="Preparing"></span> <span class="ov-bar-seg ov-bar-preparing" title="Preparing" :style="{width: barPct(&#x27;preparing&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-error" ng-style="{width: barPct('error') + '%'}" title="Error"></span> <span class="ov-bar-seg ov-bar-error" title="Error" :style="{width: barPct(&#x27;error&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-expired" ng-style="{width: barPct('expired') + '%'}" title="Expired"></span> <span class="ov-bar-seg ov-bar-expired" title="Expired" :style="{width: barPct(&#x27;expired&#x27;) + &#x27;%&#x27;}"></span>
<span class="ov-bar-seg ov-bar-removed" ng-style="{width: barPct('removed') + '%'}" title="Removed"></span> <span class="ov-bar-seg ov-bar-removed" title="Removed" :style="{width: barPct(&#x27;removed&#x27;) + &#x27;%&#x27;}"></span>
</div> </div>
<div class="ov-bar-legend"> <div class="ov-bar-legend">
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{statusCount('ready') | number}}</span></a> <a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{ fmt?.number(statusCount('ready')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{statusCount('preparing') + statusCount('download') | number}}</span></a> <a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{ fmt?.number(statusCount('preparing') + statusCount('download')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{statusCount('error') | number}}</span></a> <a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{ fmt?.number(statusCount('error')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{statusCount('expired') + statusCount('expiring') | number}}</span></a> <a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{ fmt?.number(statusCount('expired') + statusCount('expiring')) }}</span></a>
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{statusCount('removed') + statusCount('removing') | number}}</span></a> <a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{ fmt?.number(statusCount('removed') + statusCount('removing')) }}</span></a>
</div> </div>
</div> </div>
@@ -172,27 +164,27 @@
<div class="ov-panel-card"> <div class="ov-panel-card">
<div class="ov-panel-head"> <div class="ov-panel-head">
<span class="ov-panel-title">Errors &middot; 24h</span> <span class="ov-panel-title">Errors &middot; 24h</span>
<span class="ov-panel-meta">{{data.errors.last24h}} total</span> <span class="ov-panel-meta">{{ data?.errors?.last24h }} total</span>
</div> </div>
<div class="ov-error-bars"> <div class="ov-error-bars">
<div class="ov-ebar-row"> <div class="ov-ebar-row">
<span class="ov-ebar-label">5xx</span> <span class="ov-ebar-label">5xx</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" ng-style="{width: errPct('error') + '%'}"></span></span> <span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" :style="{width: errPct(&#x27;error&#x27;) + &#x27;%&#x27;}"></span></span>
<span class="ov-ebar-n">{{data.errors.severity.error}}</span> <span class="ov-ebar-n">{{ data?.errors?.severity?.error }}</span>
</div> </div>
<div class="ov-ebar-row"> <div class="ov-ebar-row">
<span class="ov-ebar-label">4xx</span> <span class="ov-ebar-label">4xx</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" ng-style="{width: errPct('warn') + '%'}"></span></span> <span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" :style="{width: errPct(&#x27;warn&#x27;) + &#x27;%&#x27;}"></span></span>
<span class="ov-ebar-n">{{data.errors.severity.warn}}</span> <span class="ov-ebar-n">{{ data?.errors?.severity?.warn }}</span>
</div> </div>
<div class="ov-ebar-row"> <div class="ov-ebar-row">
<span class="ov-ebar-label">Info</span> <span class="ov-ebar-label">Info</span>
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" ng-style="{width: errPct('info') + '%'}"></span></span> <span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" :style="{width: errPct(&#x27;info&#x27;) + &#x27;%&#x27;}"></span></span>
<span class="ov-ebar-n">{{data.errors.severity.info}}</span> <span class="ov-ebar-n">{{ data?.errors?.severity?.info }}</span>
</div> </div>
</div> </div>
<div class="ov-panel-foot" ng-if="data.repos.recentErrors24h"> <div class="ov-panel-foot" v-if="data?.repos?.recentErrors24h">
<a href="/admin/errors">{{data.repos.recentErrors24h}} repos in error state &rarr;</a> <a href="/admin/errors">{{ data?.repos?.recentErrors24h }} repos in error state &rarr;</a>
</div> </div>
</div> </div>
@@ -211,21 +203,21 @@
</div> </div>
<a class="ov-route-row" href="/admin/queues"> <a class="ov-route-row" href="/admin/queues">
<span class="ov-route-name">download</span> <span class="ov-route-name">download</span>
<span class="ov-route-n">{{data.queues.download.active}}</span> <span class="ov-route-n">{{ data?.queues?.download?.active }}</span>
<span class="ov-route-n">{{data.queues.download.waiting}}</span> <span class="ov-route-n">{{ data?.queues?.download?.waiting }}</span>
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.download.failed > 0}">{{data.queues.download.failed}}</span> <span class="ov-route-n ov-route-lat" :class="{&#x27;ov-n-bad&#x27;: data?.queues?.download?.failed &gt; 0}">{{ data?.queues?.download?.failed }}</span>
</a> </a>
<a class="ov-route-row" href="/admin/queues"> <a class="ov-route-row" href="/admin/queues">
<span class="ov-route-name">remove</span> <span class="ov-route-name">remove</span>
<span class="ov-route-n">{{data.queues.remove.active}}</span> <span class="ov-route-n">{{ data?.queues?.remove?.active }}</span>
<span class="ov-route-n">{{data.queues.remove.waiting}}</span> <span class="ov-route-n">{{ data?.queues?.remove?.waiting }}</span>
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.remove.failed > 0}">{{data.queues.remove.failed}}</span> <span class="ov-route-n ov-route-lat" :class="{&#x27;ov-n-bad&#x27;: data?.queues?.remove?.failed &gt; 0}">{{ data?.queues?.remove?.failed }}</span>
</a> </a>
<a class="ov-route-row" href="/admin/queues"> <a class="ov-route-row" href="/admin/queues">
<span class="ov-route-name">cache</span> <span class="ov-route-name">cache</span>
<span class="ov-route-n">{{data.queues.cache.active}}</span> <span class="ov-route-n">{{ data?.queues?.cache?.active }}</span>
<span class="ov-route-n">{{data.queues.cache.waiting}}</span> <span class="ov-route-n">{{ data?.queues?.cache?.waiting }}</span>
<span class="ov-route-n ov-route-lat" ng-class="{'ov-n-bad': data.queues.cache.failed > 0}">{{data.queues.cache.failed}}</span> <span class="ov-route-n ov-route-lat" :class="{&#x27;ov-n-bad&#x27;: data?.queues?.cache?.failed &gt; 0}">{{ data?.queues?.cache?.failed }}</span>
</a> </a>
</div> </div>
</div> </div>
@@ -236,46 +228,46 @@
<section class="ov-services-card"> <section class="ov-services-card">
<div class="ov-services-head"> <div class="ov-services-head">
<span class="ov-panel-title">Services</span> <span class="ov-panel-title">Services</span>
<span class="ov-panel-meta">{{data.users.total | number}} users &middot; {{data.conferences.total}} conferences</span> <span class="ov-panel-meta">{{ fmt?.number(data?.users?.total) }} users &middot; {{ data?.conferences?.total }} conferences</span>
</div> </div>
<div class="ov-services-grid"> <div class="ov-services-grid">
<div class="ov-svc"> <div class="ov-svc">
<span class="ov-svc-dot ov-dot-ok"></span> <span class="ov-svc-dot ov-dot-ok"></span>
<div class="ov-svc-info"> <div class="ov-svc-info">
<span class="ov-svc-name">web</span> <span class="ov-svc-name">web</span>
<span class="ov-svc-meta">{{data.system.nodeVersion}}</span> <span class="ov-svc-meta">{{ data?.system?.nodeVersion }}</span>
</div> </div>
<span class="ov-svc-detail">uptime {{humanDuration(data.system.uptime)}}</span> <span class="ov-svc-detail">uptime {{ humanDuration(data?.system?.uptime) }}</span>
</div> </div>
<div class="ov-svc"> <div class="ov-svc">
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.download) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span> <span class="ov-svc-dot" :class="queueTotal(data?.queues?.download) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></span>
<div class="ov-svc-info"> <div class="ov-svc-info">
<span class="ov-svc-name">download</span> <span class="ov-svc-name">download</span>
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.download)">{{queueTotal(data.queues.download)}} jobs</span> <span class="ov-svc-meta" v-if="queueTotal(data?.queues?.download)">{{ queueTotal(data?.queues?.download) }} jobs</span>
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.download)">idle</span> <span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.download)">idle</span>
</div> </div>
</div> </div>
<div class="ov-svc"> <div class="ov-svc">
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.cache) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span> <span class="ov-svc-dot" :class="queueTotal(data?.queues?.cache) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></span>
<div class="ov-svc-info"> <div class="ov-svc-info">
<span class="ov-svc-name">cache</span> <span class="ov-svc-name">cache</span>
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.cache)">{{queueTotal(data.queues.cache)}} jobs</span> <span class="ov-svc-meta" v-if="queueTotal(data?.queues?.cache)">{{ queueTotal(data?.queues?.cache) }} jobs</span>
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.cache)">idle</span> <span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.cache)">idle</span>
</div> </div>
</div> </div>
<div class="ov-svc"> <div class="ov-svc">
<span class="ov-svc-dot" ng-class="queueTotal(data.queues.remove) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span> <span class="ov-svc-dot" :class="queueTotal(data?.queues?.remove) &gt; 0 ? &#x27;ov-dot-ok&#x27; : &#x27;ov-dot-idle&#x27;"></span>
<div class="ov-svc-info"> <div class="ov-svc-info">
<span class="ov-svc-name">remove</span> <span class="ov-svc-name">remove</span>
<span class="ov-svc-meta" ng-if="queueTotal(data.queues.remove)">{{queueTotal(data.queues.remove)}} jobs</span> <span class="ov-svc-meta" v-if="queueTotal(data?.queues?.remove)">{{ queueTotal(data?.queues?.remove) }} jobs</span>
<span class="ov-svc-meta" ng-if="!queueTotal(data.queues.remove)">idle</span> <span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.remove)">idle</span>
</div> </div>
</div> </div>
<div class="ov-svc"> <div class="ov-svc">
<span class="ov-svc-dot" ng-class="data.repos.recentErrors24h > 10 ? 'ov-dot-warn' : 'ov-dot-ok'"></span> <span class="ov-svc-dot" :class="data?.repos?.recentErrors24h &gt; 10 ? &#x27;ov-dot-warn&#x27; : &#x27;ov-dot-ok&#x27;"></span>
<div class="ov-svc-info"> <div class="ov-svc-info">
<span class="ov-svc-name">errors</span> <span class="ov-svc-name">errors</span>
<span class="ov-svc-meta">{{data.errors.last24h}} / 24h</span> <span class="ov-svc-meta">{{ data?.errors?.last24h }} / 24h</span>
</div> </div>
</div> </div>
</div> </div>
+79 -79
View File
@@ -5,13 +5,13 @@
<h1 class="paper-page-title">Queues</h1> <h1 class="paper-page-title">Queues</h1>
<div class="q-header-actions"> <div class="q-header-actions">
<div class="q-range-btns"> <div class="q-range-btns">
<button class="btn btn-sm" ng-class="{active: range == '1h'}" ng-click="setRange('1h')">1h</button> <button class="btn btn-sm" :class="{active: range == &#x27;1h&#x27;}" @click="setRange(&#x27;1h&#x27;)">1h</button>
<button class="btn btn-sm" ng-class="{active: range == '6h'}" ng-click="setRange('6h')">6h</button> <button class="btn btn-sm" :class="{active: range == &#x27;6h&#x27;}" @click="setRange(&#x27;6h&#x27;)">6h</button>
<button class="btn btn-sm" ng-class="{active: range == '24h'}" ng-click="setRange('24h')">24h</button> <button class="btn btn-sm" :class="{active: range == &#x27;24h&#x27;}" @click="setRange(&#x27;24h&#x27;)">24h</button>
<button class="btn btn-sm" ng-class="{active: range == '7d'}" ng-click="setRange('7d')">7d</button> <button class="btn btn-sm" :class="{active: range == &#x27;7d&#x27;}" @click="setRange(&#x27;7d&#x27;)">7d</button>
</div> </div>
<button class="btn btn-sm" ng-click="pauseAll()">Pause all</button> <button class="btn btn-sm" @click="pauseAll()">Pause all</button>
<button class="btn btn-sm btn-dark" ng-click="drainSelected()">Drain {{selectedQueue}}</button> <button class="btn btn-sm btn-dark" @click="drainSelected()">Drain {{ selectedQueue }}</button>
</div> </div>
</div> </div>
@@ -26,25 +26,25 @@
<!-- Queue overview cards --> <!-- Queue overview cards -->
<div class="q-cards"> <div class="q-cards">
<div class="q-card" ng-repeat="q in queueList" ng-class="{selected: selectedQueue == q.key, paused: q.paused}" ng-click="selectQueue(q.key)"> <div class="q-card" v-for="(q, index) in queueList" :class="{selected: selectedQueue == q?.key, paused: q?.paused}" @click="selectQueue(q.key)">
<div class="q-card-head"> <div class="q-card-head">
<span class="q-dot" ng-class="{'q-dot-red': q.paused || q.counts.failed > 0}"></span> <span class="q-dot" :class="{&#x27;q-dot-red&#x27;: q?.paused || q?.counts?.failed &gt; 0}"></span>
<span class="q-card-name" ng-bind="q.label"></span> <span class="q-card-name" v-text="q?.label"></span>
</div> </div>
<div class="q-card-count" ng-bind="(q.counts.waiting || 0) + (q.counts.active || 0) + (q.counts.delayed || 0)"></div> <div class="q-card-count" v-text="(q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0)"></div>
<div class="q-card-sub"> <div class="q-card-sub">
<span>waiting &middot; {{q.counts.active || 0}} active</span> <span>waiting &middot; {{ q?.counts?.active || 0 }} active</span>
<div class="q-card-bar" ng-if="q.counts.active"> <div class="q-card-bar" v-if="q?.counts?.active">
<div class="q-card-bar-fill" ng-style="{width: (q.counts.active / ((q.counts.waiting || 0) + (q.counts.active || 0) + (q.counts.delayed || 0) || 1) * 100) + '%'}"></div> <div class="q-card-bar-fill" :style="{width: (q?.counts?.active / ((q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0) || 1) * 100) + &#x27;%&#x27;}"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Detail: throughput chart + stats panel --> <!-- Detail: throughput chart + stats panel -->
<div class="q-detail" ng-if="selectedStats"> <div class="q-detail" v-if="selectedStats">
<div class="q-throughput"> <div class="q-throughput">
<div class="q-section-label">{{selectedQueue}}&middot;throughput <span class="q-section-right"><span class="q-legend-completed">&#9679;</span> completed <span class="q-legend-failed">&#9679;</span> failed <span class="q-legend-exec">- -</span> avg time &middot; {{range | uppercase}}</span></div> <div class="q-section-label">{{ selectedQueue }}&middot;throughput <span class="q-section-right"><span class="q-legend-completed">&#9679;</span> completed <span class="q-legend-failed">&#9679;</span> failed <span class="q-legend-exec">- -</span> avg time &middot; {{ fmt?.uppercase(range) }}</span></div>
<div class="q-chart-wrap"> <div class="q-chart-wrap">
<canvas id="q-throughput-chart" height="180"></canvas> <canvas id="q-throughput-chart" height="180"></canvas>
<div id="q-chart-tooltip" class="q-chart-tooltip" style="display:none;"></div> <div id="q-chart-tooltip" class="q-chart-tooltip" style="display:none;"></div>
@@ -52,67 +52,67 @@
</div> </div>
</div> </div>
<div class="q-stats-panel"> <div class="q-stats-panel">
<div class="q-section-label">{{selectedQueue}}&middot;stats</div> <div class="q-section-label">{{ selectedQueue }}&middot;stats</div>
<div class="q-stats-grid"> <div class="q-stats-grid">
<div class="q-stat"> <div class="q-stat">
<div class="q-stat-label">WAITING</div> <div class="q-stat-label">WAITING</div>
<div class="q-stat-value" ng-bind="selectedStats.counts.waiting || 0"></div> <div class="q-stat-value" v-text="selectedStats?.counts?.waiting || 0"></div>
</div> </div>
<div class="q-stat"> <div class="q-stat">
<div class="q-stat-label">ACTIVE</div> <div class="q-stat-label">ACTIVE</div>
<div class="q-stat-value" ng-bind="selectedStats.counts.active || 0"></div> <div class="q-stat-value" v-text="selectedStats?.counts?.active || 0"></div>
</div> </div>
<div class="q-stat"> <div class="q-stat">
<div class="q-stat-label">COMPLETED (24H)</div> <div class="q-stat-label">COMPLETED (24H)</div>
<div class="q-stat-value" ng-bind="selectedStats.completed24h | number"></div> <div class="q-stat-value" v-text="fmt?.number(selectedStats?.completed24h)"></div>
</div> </div>
<div class="q-stat"> <div class="q-stat">
<div class="q-stat-label">FAILED (24H)</div> <div class="q-stat-label">FAILED (24H)</div>
<div class="q-stat-value" ng-bind="selectedStats.failed24h || 0"></div> <div class="q-stat-value" v-text="selectedStats?.failed24h || 0"></div>
</div> </div>
<div class="q-stat"> <div class="q-stat">
<div class="q-stat-label">DELAYED</div> <div class="q-stat-label">DELAYED</div>
<div class="q-stat-value" ng-bind="selectedStats.counts.delayed || 0"></div> <div class="q-stat-value" v-text="selectedStats?.counts?.delayed || 0"></div>
</div> </div>
<div class="q-stat"> <div class="q-stat">
<div class="q-stat-label">WORKERS</div> <div class="q-stat-label">WORKERS</div>
<div class="q-stat-value" ng-bind="selectedStats.workers || 0"></div> <div class="q-stat-value" v-text="selectedStats?.workers || 0"></div>
</div> </div>
</div> </div>
<div class="q-stats-actions"> <div class="q-stats-actions">
<button class="btn btn-sm" ng-click="togglePause()">{{selectedStats.paused ? 'Resume' : 'Pause'}}</button> <button class="btn btn-sm" @click="togglePause()">{{ selectedStats?.paused ? 'Resume' : 'Pause' }}</button>
<button class="btn btn-sm" ng-click="retryFailed()" ng-disabled="!selectedStats.counts.failed">Retry failed</button> <button class="btn btn-sm" @click="retryFailed()" :disabled="!selectedStats?.counts?.failed">Retry failed</button>
<button class="btn btn-sm" ng-click="emptyQueue()">Empty</button> <button class="btn btn-sm" @click="emptyQueue()">Empty</button>
</div> </div>
</div> </div>
</div> </div>
<div class="q-toast-error" ng-if="actionError" ng-click="actionError = null"> <div class="q-toast-error" v-if="actionError" @click="actionError = null">
<i class="fas fa-exclamation-circle"></i> {{actionError}} <i class="fas fa-exclamation-circle"></i> {{ actionError }}
</div> </div>
<!-- Jobs table --> <!-- Jobs table -->
<div class="q-jobs"> <div class="q-jobs">
<div class="q-jobs-header"> <div class="q-jobs-header">
<div class="q-section-label">ALL JOBS &middot; {{selectedQueue | uppercase}}</div> <div class="q-section-label">ALL JOBS &middot; {{ fmt?.uppercase(selectedQueue) }}</div>
<div class="q-state-filters"> <div class="q-state-filters">
<label class="q-state-toggle" ng-repeat="s in allStates"> <label class="q-state-toggle" v-for="(s, index) in allStates">
<input type="checkbox" ng-model="stateFilter[s]" /> <input type="checkbox" v-field="{ state: viewState, set: value =&gt; { stateFilter[s] = value }, value: stateFilter[s], form: null, options: {} }">
<span class="q-state-chip q-state-{{s}}">{{s}}</span> <span :class="&quot;q-state-chip q-state-&quot; + (s)">{{ s }}</span>
</label> </label>
</div> </div>
</div> </div>
<div class="q-search-row"> <div class="q-search-row">
<input type="search" class="form-control" placeholder="Search by job/repo id…" ng-model="query.search" autocomplete="off" /> <input type="search" class="form-control" placeholder="Search by job/repo id…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<label class="q-auto-refresh"> <label class="q-auto-refresh">
<input type="checkbox" ng-model="query.autoRefresh" /> <input type="checkbox" v-field="{ state: viewState, set: value =&gt; { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
Auto-refresh Auto-refresh
</label> </label>
<button class="btn btn-sm" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button> <button class="btn btn-sm" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
</div> </div>
<table class="q-table" ng-if="filteredJobs().length > 0"> <table class="q-table" v-if="filteredJobs().length &gt; 0">
<thead> <thead>
<tr> <tr>
<th>STATE</th> <th>STATE</th>
@@ -124,90 +124,90 @@
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody ng-repeat="job in filteredJobs()"> <tbody v-for="(job, index) in filteredJobs()">
<tr ng-class="{'q-row-failed': job._state == 'failed', 'q-row-expanded': expanded[job.id]}" ng-click="toggleJob(job)" style="cursor:pointer;"> <tr style="cursor:pointer;" :class="{&#x27;q-row-failed&#x27;: job?._state == &#x27;failed&#x27;, &#x27;q-row-expanded&#x27;: expanded[job?.id]}" @click="toggleJob(job)">
<td class="q-cell-state"> <td class="q-cell-state">
<span class="q-state-badge q-state-{{job._state}}" ng-bind="job._state"></span> <span v-text="job?._state" :class="&quot;q-state-badge q-state-&quot; + (job?._state)"></span>
<span class="q-delay-hint" ng-if="job._state == 'delayed' && job.delayUntil" ng-bind="delayCountdown(job.delayUntil)"></span> <span class="q-delay-hint" v-if="job?._state == &#x27;delayed&#x27; &amp;&amp; job?.delayUntil" v-text="delayCountdown(job?.delayUntil)"></span>
</td> </td>
<td class="q-cell-id"> <td class="q-cell-id">
<i class="fas fa-chevron-right q-chevron" ng-class="{'q-chevron-open': expanded[job.id]}"></i> <i class="fas fa-chevron-right q-chevron" :class="{&#x27;q-chevron-open&#x27;: expanded[job?.id]}"></i>
<a target="_blank" ng-href="/r/{{job.id}}" ng-click="$event.stopPropagation()" ng-bind="'job:' + (job.id | limitTo:6)"></a> <a target="_blank" v-text="&#x27;job:&#x27; + fmt.limitTo(job?.id, 6)" @click="$event.stopPropagation()" :href="safeUrl(&quot;/r/&quot; + (job?.id))"></a>
</td> </td>
<td class="q-cell-payload"> <td class="q-cell-payload">
<span ng-bind="job.name || 'anonymize'"></span> <span v-text="job?.name || &#x27;anonymize&#x27;"></span>
<span class="q-payload-detail" ng-if="job.data.repoId && job.data.repoId !== job.name"> &middot; {{job.data.repoId}}</span> <span class="q-payload-detail" v-if="job?.data?.repoId &amp;&amp; job?.data?.repoId !== job?.name"> &middot; {{ job?.data?.repoId }}</span>
</td> </td>
<td class="q-cell-num" ng-bind="job.attemptsMade || 1"></td> <td class="q-cell-num" v-text="job?.attemptsMade || 1"></td>
<td class="q-cell-num" ng-bind="jobDuration(job)"></td> <td class="q-cell-num" v-text="jobDuration(job)"></td>
<td class="q-cell-progress"> <td class="q-cell-progress">
<div class="q-progress-wrap" ng-if="jobProgressPct(job) !== null"> <div class="q-progress-wrap" v-if="jobProgressPct(job) !== null">
<div class="q-progress-bar" ng-style="{'--pct': jobProgressPct(job) + '%'}"></div> <div class="q-progress-bar" :style="{&#x27;--pct&#x27;: jobProgressPct(job) + &#x27;%&#x27;}"></div>
<span class="q-progress-label" ng-bind="jobProgressPct(job) + '%'"></span> <span class="q-progress-label" v-text="jobProgressPct(job) + &#x27;%&#x27;"></span>
</div> </div>
</td> </td>
<td class="q-cell-actions"> <td class="q-cell-actions">
<button class="btn btn-sm" ng-click="retryJob(job); $event.stopPropagation()" title="Retry" ng-if="job._state == 'failed'"><i class="fas fa-sync"></i></button> <button class="btn btn-sm" title="Retry" v-if="job?._state == &#x27;failed&#x27;" @click="retryJob(job); $event.stopPropagation()"><i class="fas fa-sync"></i></button>
<button class="btn btn-sm" ng-click="removeJob(job); $event.stopPropagation()" title="Remove"><i class="fas fa-trash-alt"></i></button> <button class="btn btn-sm" title="Remove" @click="removeJob(job); $event.stopPropagation()"><i class="fas fa-trash-alt"></i></button>
</td> </td>
</tr> </tr>
<tr class="q-detail-row" ng-if="expanded[job.id]"> <tr class="q-detail-row" v-if="expanded[job?.id]">
<td colspan="7"> <td colspan="7">
<div class="q-job-detail"> <div class="q-job-detail">
<div class="q-job-detail-grid"> <div class="q-job-detail-grid">
<div class="q-job-detail-item"> <div class="q-job-detail-item">
<span class="q-job-detail-label">JOB ID</span> <span class="q-job-detail-label">JOB ID</span>
<span class="q-job-detail-value"><a target="_blank" ng-href="/r/{{job.id}}" ng-bind="job.id"></a></span> <span class="q-job-detail-value"><a target="_blank" v-text="job?.id" :href="safeUrl(&quot;/r/&quot; + (job?.id))"></a></span>
</div> </div>
<div class="q-job-detail-item"> <div class="q-job-detail-item">
<span class="q-job-detail-label">STATE</span> <span class="q-job-detail-label">STATE</span>
<span class="q-job-detail-value"><span class="q-state-badge q-state-{{job._state}}" ng-bind="job._state"></span></span> <span class="q-job-detail-value"><span v-text="job?._state" :class="&quot;q-state-badge q-state-&quot; + (job?._state)"></span></span>
</div> </div>
<div class="q-job-detail-item" ng-if="job.data.repoId"> <div class="q-job-detail-item" v-if="job?.data?.repoId">
<span class="q-job-detail-label">REPO ID</span> <span class="q-job-detail-label">REPO ID</span>
<span class="q-job-detail-value" ng-bind="job.data.repoId"></span> <span class="q-job-detail-value" v-text="job?.data?.repoId"></span>
</div> </div>
<div class="q-job-detail-item" ng-if="job.timestamp"> <div class="q-job-detail-item" v-if="job?.timestamp">
<span class="q-job-detail-label">CREATED</span> <span class="q-job-detail-label">CREATED</span>
<span class="q-job-detail-value" ng-bind="humanTime(job.timestamp)"></span> <span class="q-job-detail-value" v-text="humanTime(job?.timestamp)"></span>
</div> </div>
<div class="q-job-detail-item" ng-if="job._state == 'delayed' && job.delayUntil"> <div class="q-job-detail-item" v-if="job?._state == &#x27;delayed&#x27; &amp;&amp; job?.delayUntil">
<span class="q-job-detail-label">RETRY AT</span> <span class="q-job-detail-label">RETRY AT</span>
<span class="q-job-detail-value">{{humanTime(job.delayUntil)}} ({{delayCountdown(job.delayUntil)}})</span> <span class="q-job-detail-value">{{ humanTime(job?.delayUntil) }} ({{ delayCountdown(job?.delayUntil) }})</span>
</div> </div>
<div class="q-job-detail-item" ng-if="job.processedOn"> <div class="q-job-detail-item" v-if="job?.processedOn">
<span class="q-job-detail-label">PROCESSED</span> <span class="q-job-detail-label">PROCESSED</span>
<span class="q-job-detail-value" ng-bind="humanTime(job.processedOn)"></span> <span class="q-job-detail-value" v-text="humanTime(job?.processedOn)"></span>
</div> </div>
<div class="q-job-detail-item" ng-if="job.finishedOn"> <div class="q-job-detail-item" v-if="job?.finishedOn">
<span class="q-job-detail-label">FINISHED</span> <span class="q-job-detail-label">FINISHED</span>
<span class="q-job-detail-value" ng-bind="humanTime(job.finishedOn)"></span> <span class="q-job-detail-value" v-text="humanTime(job?.finishedOn)"></span>
</div> </div>
<div class="q-job-detail-item" ng-if="job.attemptsMade"> <div class="q-job-detail-item" v-if="job?.attemptsMade">
<span class="q-job-detail-label">ATTEMPTS</span> <span class="q-job-detail-label">ATTEMPTS</span>
<span class="q-job-detail-value" ng-bind="job.attemptsMade"></span> <span class="q-job-detail-value" v-text="job?.attemptsMade"></span>
</div> </div>
<div class="q-job-detail-item" ng-if="job.progress && job.progress.status"> <div class="q-job-detail-item" v-if="job?.progress &amp;&amp; job?.progress?.status">
<span class="q-job-detail-label">STATUS</span> <span class="q-job-detail-label">STATUS</span>
<span class="q-job-detail-value" ng-bind="job.progress.status"></span> <span class="q-job-detail-value" v-text="job?.progress?.status"></span>
</div> </div>
<div class="q-job-detail-item" ng-if="jobProgressPct(job) !== null"> <div class="q-job-detail-item" v-if="jobProgressPct(job) !== null">
<span class="q-job-detail-label">PROGRESS</span> <span class="q-job-detail-label">PROGRESS</span>
<span class="q-job-detail-value" ng-bind="jobProgressPct(job) + '%'"></span> <span class="q-job-detail-value" v-text="jobProgressPct(job) + &#x27;%&#x27;"></span>
</div> </div>
</div> </div>
<div ng-if="job.failedReason" class="q-job-detail-error"> <div class="q-job-detail-error" v-if="job?.failedReason">
<span class="q-job-detail-label">ERROR</span> <span class="q-job-detail-label">ERROR</span>
<div class="q-error-reason" ng-bind="job.failedReason"></div> <div class="q-error-reason" v-text="job?.failedReason"></div>
</div> </div>
<div ng-if="job.stacktrace.length"> <div v-if="job?.stacktrace?.length">
<span class="q-job-detail-label">STACKTRACE</span> <span class="q-job-detail-label">STACKTRACE</span>
<pre ng-repeat="stack in job.stacktrace track by $index" class="q-error-stack"><code ng-bind="stack"></code></pre> <pre class="q-error-stack" v-for="(stack, index) in job?.stacktrace" :key="index"><code v-text="stack"></code></pre>
</div> </div>
<div class="q-job-detail-actions"> <div class="q-job-detail-actions">
<button class="btn btn-sm" ng-click="retryJob(job)" ng-if="job._state == 'failed'"><i class="fas fa-sync"></i> Retry</button> <button class="btn btn-sm" v-if="job?._state == &#x27;failed&#x27;" @click="retryJob(job)"><i class="fas fa-sync"></i> Retry</button>
<button class="btn btn-sm" ng-click="removeJob(job)"><i class="fas fa-trash-alt"></i> Remove</button> <button class="btn btn-sm" @click="removeJob(job)"><i class="fas fa-trash-alt"></i> Remove</button>
<a class="btn btn-sm" target="_blank" ng-href="/r/{{job.id}}"><i class="fas fa-external-link-alt"></i> View repo</a> <a class="btn btn-sm" target="_blank" :href="safeUrl(&quot;/r/&quot; + (job?.id))"><i class="fas fa-external-link-alt"></i> View repo</a>
</div> </div>
</div> </div>
</td> </td>
@@ -215,10 +215,10 @@
</tbody> </tbody>
</table> </table>
<div class="paper-table-empty" ng-if="filteredJobs().length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);"> <div class="paper-table-empty" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);" v-if="filteredJobs().length == 0">
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
<span ng-if="!query.search">No jobs in the {{selectedQueue}} queue.</span> <span v-if="!query?.search">No jobs in the {{ selectedQueue }} queue.</span>
<span ng-if="query.search">No jobs match the current filters.</span> <span v-if="query?.search">No jobs match the current filters.</span>
</div> </div>
</div> </div>
</div> </div>
+59 -71
View File
@@ -12,143 +12,131 @@
</nav> </nav>
<div class="admin-summary"> <div class="admin-summary">
<span class="summary-total">{{total >= 0 ? (total | number) : '…'}}</span> <span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
<span class="summary-meta">{{totalSize | humanFileSize}} on disk</span> <span class="summary-meta">{{ fmt?.humanFileSize(totalSize) }} on disk</span>
<span class="summary-pill ok" ng-class="{active: query.ready}" ng-click="query.ready = !query.ready; query.page = 1" title="Toggle ready filter">Ready <span class="count">{{statusCountFor('ready') | number}}</span></span> <span class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></span>
<span class="summary-pill warn" ng-class="{active: query.preparing}" ng-click="query.preparing = !query.preparing; query.page = 1" title="Toggle preparing filter">Preparing <span class="count">{{statusCountFor('preparing') + statusCountFor('download') | number}}</span></span> <span class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing') + statusCountFor('download')) }}</span></span>
<span class="summary-pill error" ng-class="{active: query.error}" ng-click="query.error = !query.error; query.page = 1" title="Toggle errored filter">Errored <span class="count">{{statusCountFor('error') | number}}</span></span> <span class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></span>
<span class="summary-pill" ng-class="{active: query.expired}" ng-click="query.expired = !query.expired; query.page = 1" title="Toggle expired filter">Expired <span class="count">{{statusCountFor('expired') + statusCountFor('expiring') | number}}</span></span> <span class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1">Expired <span class="count">{{ fmt?.number(statusCountFor('expired') + statusCountFor('expiring')) }}</span></span>
<span class="summary-pill" ng-class="{active: query.removed}" ng-click="query.removed = !query.removed; query.page = 1" title="Toggle removed filter">Removed <span class="count">{{statusCountFor('removed') + statusCountFor('removing') | number}}</span></span> <span class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed') + statusCountFor('removing')) }}</span></span>
</div> </div>
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;"> <div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
<i class="fas fa-exclamation-triangle"></i> {{fetchError}} <i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
</div> </div>
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8"> <form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
<!-- Row 1: search + scoped inputs + headline actions --> <!-- Row 1: search + scoped inputs + headline actions -->
<div class="admin-filter-row"> <div class="admin-filter-row">
<div class="search-wrap"> <div class="search-wrap">
<input <input type="search" class="form-control" aria-label="Search repositories" placeholder="Search repoId, source repo, error message…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
type="search" <span class="admin-search-hint" v-if="!query?.search">/</span>
class="form-control"
aria-label="Search repositories"
placeholder="Search repoId, source repo, error message…"
autocomplete="off"
ng-model="query.search"
/>
<span class="admin-search-hint" ng-if="!query.search">/</span>
</div> </div>
<span class="admin-filter-inline"><label>Owner</label><input type="text" class="form-control form-control-sm" placeholder="username" ng-model="query.owner" /></span> <span class="admin-filter-inline"><label>Owner</label><input type="text" class="form-control form-control-sm" placeholder="username" v-field="{ state: viewState, set: value =&gt; { query.owner = value }, value: query?.owner, form: null, options: {} }"></span>
<span class="admin-filter-inline"><label>Conference</label><input type="text" class="form-control form-control-sm" placeholder="ID" ng-model="query.conference" /></span> <span class="admin-filter-inline"><label>Conference</label><input type="text" class="form-control form-control-sm" placeholder="ID" v-field="{ state: viewState, set: value =&gt; { query.conference = value }, value: query?.conference, form: null, options: {} }"></span>
<span class="admin-filter-spacer"></span> <span class="admin-filter-spacer"></span>
<button class="btn btn-sm" type="button" ng-click="exportCsv()" title="Export current view to CSV"><i class="fas fa-file-csv"></i> Export</button> <button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
<span class="admin-filter-inline" aria-label="Pagination"> <span class="admin-filter-inline" aria-label="Pagination">
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i></button> <button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 1"><i class="fas fa-chevron-left"></i></button>
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{query.page}}/{{totalPage || 1}}</span> <span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{ query?.page }}/{{ totalPage || 1 }}</span>
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage"><i class="fas fa-chevron-right"></i></button> <button class="btn btn-sm" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page &gt;= totalPage"><i class="fas fa-chevron-right"></i></button>
</span> </span>
</div> </div>
<!-- Row 2: appears only when there are active filter chips --> <!-- Row 2: appears only when there are active filter chips -->
<div class="admin-filter-row" ng-if="chips.length"> <div class="admin-filter-row" v-if="chips?.length">
<div class="admin-active-chips"> <div class="admin-active-chips">
<span class="admin-active-chip" ng-repeat="chip in chips track by chip.key"> <span class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
<span class="key">{{chip.label}}</span> <span class="key">{{ chip?.label }}</span>
<span>{{chip.value}}</span> <span>{{ chip?.value }}</span>
<button type="button" ng-click="clearFilter(chip.key)" aria-label="Remove filter"><i class="fas fa-times"></i></button> <button type="button" aria-label="Remove filter" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
</span> </span>
</div> </div>
</div> </div>
</form> </form>
<div class="bulk-bar" ng-if="selectedCount() > 0"> <div class="bulk-bar" v-if="selectedCount() &gt; 0">
<span><strong>{{selectedCount()}}</strong> selected</span> <span><strong>{{ selectedCount() }}</strong> selected</span>
<button class="btn btn-sm" type="button" ng-click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button> <button class="btn btn-sm" type="button" @click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button>
<button class="btn btn-sm text-danger" type="button" ng-click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button> <button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
<button class="btn btn-sm" type="button" ng-click="clearSelection()">Clear</button> <button class="btn btn-sm" type="button" @click="clearSelection()">Clear</button>
</div> </div>
<div class="paper-table paper-table-repos has-bulk w-100" role="table" aria-label="Repositories"> <div class="paper-table paper-table-repos has-bulk w-100" role="table" aria-label="Repositories">
<div class="paper-table-head" role="row"> <div class="paper-table-head" role="row">
<div role="columnheader" style="width: 28px;"> <div role="columnheader" style="width: 28px;">
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" /> <input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
</div> </div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'source.repositoryName'}" ng-click="sortBy('source.repositoryName')">Repository <i class="fas" ng-class="sortIcon('source.repositoryName')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;source.repositoryName&#x27;}" @click="sortBy(&#x27;source.repositoryName&#x27;)">Repository <i class="fas" :class="sortIcon(&#x27;source.repositoryName&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'status'}" ng-click="sortBy('status')">Status <i class="fas" ng-class="sortIcon('status')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></i></span></div>
<div role="columnheader" class="num"><span class="sortable" ng-class="{active: query.sort == 'pageView'}" ng-click="sortBy('pageView')">Views <i class="fas" ng-class="sortIcon('pageView')"></i></span></div> <div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == &#x27;pageView&#x27;}" @click="sortBy(&#x27;pageView&#x27;)">Views <i class="fas" :class="sortIcon(&#x27;pageView&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'anonymizeDate'}" ng-click="sortBy('anonymizeDate')">Anonymized <i class="fas" ng-class="sortIcon('anonymizeDate')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;anonymizeDate&#x27;}" @click="sortBy(&#x27;anonymizeDate&#x27;)">Anonymized <i class="fas" :class="sortIcon(&#x27;anonymizeDate&#x27;)"></i></span></div>
<div role="columnheader" aria-label="Actions"></div> <div role="columnheader" aria-label="Actions"></div>
</div> </div>
<div <div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{&#x27;repo-inactive&#x27;: repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;, &#x27;repo-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;row-selected&#x27;: selected[repo?.repoId]}">
class="paper-table-row"
role="row"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'repo-error': repo.status == 'error', 'row-selected': selected[repo.repoId]}"
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
>
<div role="cell" style="width: 28px;"> <div role="cell" style="width: 28px;">
<input type="checkbox" ng-model="selected[repo.repoId]" aria-label="Select repository" /> <input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value =&gt; { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
</div> </div>
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<span class="type-badge type-repo">Repo</span> <span class="type-badge type-repo">Repo</span>
<div class="anon-text"> <div class="anon-text">
<a class="repo-name" target="_blank" ng-href="/r/{{repo.repoId}}" ng-bind="repo.repoId"></a> <a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId))"></a>
<div class="anon-sub"> <div class="anon-sub">
<a href="https://github.com/{{repo.source.repositoryName}}/" ng-bind="repo.source.repositoryName"></a><span ng-if="repo.options.update">&nbsp;&middot;&nbsp;<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.branch}}" ng-bind="repo.source.branch"></a></span><span ng-if="!repo.options.update">&nbsp;&middot;&nbsp;@<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.commit}}" ng-bind="repo.source.commit.substring(0, 8)"></a></span><span ng-if="::repo.conference">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{repo.conference}}</span><span>&nbsp;&middot;&nbsp;{{::repo.size.storage | humanFileSize}}</span><span>&nbsp;&middot;&nbsp;{{::repo.options.terms.length | number}} terms</span> <a v-text="repo?.source?.repositoryName" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.repositoryName) + &quot;/&quot;)"></a><span v-if="repo?.options?.update">&nbsp;&middot;&nbsp;<a v-text="repo?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update">&nbsp;&middot;&nbsp;@<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.commit))"></a></span><span v-if="repo?.conference">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span>&nbsp;&middot;&nbsp;{{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span>&nbsp;&middot;&nbsp;{{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
</div> </div>
</div> </div>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<span class="status-line"> <span class="status-line">
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-preparing': repo.status == 'preparing'}"></span> <span class="status-dot" :class="{&#x27;status-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-preparing&#x27;: repo?.status == &#x27;preparing&#x27;}"></span>
<span ng-bind="repo.status | title"></span> <span v-text="fmt?.title(repo?.status)"></span>
</span> </span>
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage | statusMsg"></span> <span class="status-sub" v-if="repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="(repo?.statusMessage)"></span>
</div> </div>
<div class="cell-views num" role="cell" ng-bind="::repo.pageView || 0 | number"></div> <div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView || 0)"></div>
<div class="cell-expires" role="cell" ng-bind="repo.anonymizeDate | humanTime"></div> <div class="cell-expires" role="cell" v-text="fmt?.humanTime(repo?.anonymizeDate)"></div>
<div class="cell-actions" role="cell"> <div class="cell-actions" role="cell">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions"> <button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
<i class="fas fa-ellipsis-h" aria-hidden="true"></i> <i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/anonymize/{{repo.repoId}}"><i class="far fa-edit"></i> Edit</a> <a class="dropdown-item" :href="safeUrl(&quot;/anonymize/&quot; + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
<a class="dropdown-item" href="/r/{{repo.repoId}}/"><i class="fa fa-eye"></i> View repo</a> <a class="dropdown-item" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)"><i class="fa fa-eye"></i> View repo</a>
<a class="dropdown-item" href="/w/{{repo.repoId}}/" target="_self" ng-if="repo.options.page && repo.status == 'ready'"><i class="fas fa-globe"></i> View page</a> <a class="dropdown-item" target="_self" v-if="repo?.options?.page &amp;&amp; repo?.status == &#x27;ready&#x27;" :href="safeUrl(&quot;/w/&quot; + (repo?.repoId) + &quot;/&quot;)"><i class="fas fa-globe"></i> View page</a>
<a class="dropdown-item" href="#" ng-click="fetchGithubInfo(repo)"><i class="fab fa-github"></i> Live GitHub info</a> <a class="dropdown-item" href="#" @click.prevent="fetchGithubInfo(repo)"><i class="fab fa-github"></i> Live GitHub info</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" ng-show="repo.status == 'ready' || repo.status == 'error'" ng-click="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a> <a class="dropdown-item" href="#" v-show="repo?.status == &#x27;ready&#x27; || repo?.status == &#x27;error&#x27;" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
<a class="dropdown-item" href="#" ng-show="repo.status == 'removed'" ng-click="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a> <a class="dropdown-item" href="#" v-show="repo?.status == &#x27;removed&#x27;" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
<a class="dropdown-item" href="#" ng-show="repo.statusMessage" ng-click="showStatusMessage(repo)"><i class="fas fa-exclamation-triangle"></i> View status message</a> <a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><i class="fas fa-exclamation-triangle"></i> View status message</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" ng-click="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a> <a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
<a class="dropdown-item text-danger" href="#" ng-show="repo.status == 'ready'" ng-click="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a> <a class="dropdown-item text-danger" href="#" v-show="repo?.status == &#x27;ready&#x27;" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="paper-table-empty" ng-if="filteredRepositories.length == 0"> <div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
<i class="fas fa-inbox"></i> <i class="fas fa-inbox"></i>
<span>No repositories match the current filters.</span> <span>No repositories match the current filters.</span>
</div> </div>
</div> </div>
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;"> <div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
<span style="font-size: 12px; color: var(--ink-muted);">{{total | number}} results</span> <span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
<div class="pagination-compact" ng-if="totalPage > 1"> <div class="pagination-compact" v-if="totalPage &gt; 1">
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"> <button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 1">
<i class="fas fa-chevron-left"></i> Previous <i class="fas fa-chevron-left"></i> Previous
</button> </button>
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" style="width: 56px;" aria-label="Page" /> <input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" aria-label="Page" v-field="{ state: viewState, set: value =&gt; { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
<span>of {{totalPage}}</span> <span>of {{ totalPage }}</span>
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage"> <button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page &gt;= totalPage">
Next <i class="fas fa-chevron-right"></i> Next <i class="fas fa-chevron-right"></i>
</button> </button>
</div> </div>
<span class="admin-filter-inline"> <span class="admin-filter-inline">
<label>Per page</label> <label>Per page</label>
<select class="form-control form-control-sm" ng-model="query.limit"> <select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.limit = value }, value: query?.limit, form: null, options: {} }">
<option value="10">10</option> <option value="10">10</option>
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
+82 -94
View File
@@ -1,6 +1,6 @@
<div class="container paper-page admin-page"> <div class="container paper-page admin-page">
<div class="paper-crumbs"><a href="/admin/users">Users</a> &nbsp;/&nbsp; <span class="here">{{userInfo.username || 'Profile'}}</span></div> <div class="paper-crumbs"><a href="/admin/users">Users</a> &nbsp;/&nbsp; <span class="here">{{ userInfo?.username || 'Profile' }}</span></div>
<h1 class="paper-page-title">{{userInfo.username || 'User'}}</h1> <h1 class="paper-page-title">{{ userInfo?.username || 'User' }}</h1>
<nav class="admin-nav"> <nav class="admin-nav">
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a> <a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
@@ -11,119 +11,119 @@
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a> <a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav> </nav>
<div class="user-detail-card" ng-if="userInfo"> <div class="user-detail-card" v-if="userInfo">
<div class="user-header"> <div class="user-header">
<img ng-src="{{userInfo.photo}}" ng-if="userInfo.photo" width="56" height="56" /> <img width="56" height="56" v-if="userInfo?.photo" :src="safeUrl((userInfo?.photo))">
<div> <div>
<h1> <h1>
{{userInfo.username}} {{ userInfo?.username }}
<span class="status-dot-wrap"> <span class="status-dot-wrap">
<span class="status-dot" ng-class="{'status-ready': userInfo.status == 'active', 'status-removed': userInfo.status != 'active'}"></span> <span class="status-dot" :class="{&#x27;status-ready&#x27;: userInfo?.status == &#x27;active&#x27;, &#x27;status-removed&#x27;: userInfo?.status != &#x27;active&#x27;}"></span>
<span ng-bind="userInfo.status | title"></span> <span v-text="fmt?.title(userInfo?.status)"></span>
</span> </span>
<span class="type-badge type-repo" ng-if="userInfo.isAdmin">Admin</span> <span class="type-badge type-repo" v-if="userInfo?.isAdmin">Admin</span>
</h1> </h1>
<div class="user-actions" style="margin-top: 4px;"> <div class="user-actions" style="margin-top: 4px;">
<button class="btn btn-sm text-danger" ng-if="userInfo.status !== 'banned'" ng-click="banUser()"><i class="fas fa-ban"></i> Ban</button> <button class="btn btn-sm text-danger" v-if="userInfo?.status !== &#x27;banned&#x27;" @click="banUser()"><i class="fas fa-ban"></i> Ban</button>
<button class="btn btn-sm" ng-if="userInfo.status === 'banned' || userInfo.status === 'removed'" ng-click="activateUser()"><i class="fas fa-check-circle"></i> Activate</button> <button class="btn btn-sm" v-if="userInfo?.status === &#x27;banned&#x27; || userInfo?.status === &#x27;removed&#x27;" @click="activateUser()"><i class="fas fa-check-circle"></i> Activate</button>
<button class="btn btn-sm" ng-if="!userInfo.isAdmin" ng-click="promoteUser()"><i class="fas fa-user-shield"></i> Promote to admin</button> <button class="btn btn-sm" v-if="!userInfo?.isAdmin" @click="promoteUser()"><i class="fas fa-user-shield"></i> Promote to admin</button>
<button class="btn btn-sm text-danger" ng-if="userInfo.isAdmin" ng-click="demoteUser()"><i class="fas fa-user-minus"></i> Remove admin</button> <button class="btn btn-sm text-danger" v-if="userInfo?.isAdmin" @click="demoteUser()"><i class="fas fa-user-minus"></i> Remove admin</button>
</div> </div>
</div> </div>
</div> </div>
<div class="user-detail-grid"> <div class="user-detail-grid">
<div class="detail-label">ID</div> <div class="detail-label">ID</div>
<div class="detail-value">{{userInfo._id}}</div> <div class="detail-value">{{ userInfo?._id }}</div>
<div class="detail-label">Email</div> <div class="detail-label">Email</div>
<div class="detail-value">{{userInfo.emails[0].email}}</div> <div class="detail-value">{{ userInfo?.emails?.[0]?.email }}</div>
<div class="detail-label">Access token</div> <div class="detail-label">Access token</div>
<div class="detail-value" style="font-family: var(--font-mono); font-size: 0.85rem;">{{userInfo.accessTokens.github}}</div> <div class="detail-value" style="font-family: var(--font-mono); font-size: 0.85rem;">{{ userInfo?.accessTokens?.github }}</div>
<div class="detail-label">GitHub</div> <div class="detail-label">GitHub</div>
<div class="detail-value"> <div class="detail-value">
<a ng-href="https://github.com/{{userInfo.username}}" target="_blank"> <a target="_blank" :href="safeUrl(&quot;https://github.com/&quot; + (userInfo?.username))">
<i class="fab fa-github"></i> {{userInfo.username}} <i class="fab fa-github"></i> {{ userInfo?.username }}
</a> </a>
</div> </div>
<div class="detail-label">Created</div> <div class="detail-label">Created</div>
<div class="detail-value">{{userInfo.dateOfEntry | humanTime}}</div> <div class="detail-value">{{ fmt?.humanTime(userInfo?.dateOfEntry) }}</div>
<div class="detail-label">Last connection</div> <div class="detail-label">Last connection</div>
<div class="detail-value"> <div class="detail-value">
<span ng-if="userInfo.accessTokenDates.github">{{userInfo.accessTokenDates.github | humanTime}}</span> <span v-if="userInfo?.accessTokenDates?.github">{{ fmt?.humanTime(userInfo?.accessTokenDates?.github) }}</span>
<span class="text-muted" ng-if="!userInfo.accessTokenDates.github">never</span> <span class="text-muted" v-if="!userInfo?.accessTokenDates?.github">never</span>
</div> </div>
<div class="detail-label">GitHub repos</div> <div class="detail-label">GitHub repos</div>
<div class="detail-value"> <div class="detail-value">
{{userInfo.repositories.length}} repositories {{ userInfo?.repositories?.length }} repositories
<button class="btn btn-sm ml-2" ng-click="showRepos = !showRepos"> <button class="btn btn-sm ml-2" @click="showRepos = !showRepos">
{{showRepos ? 'Hide' : 'Show'}} {{ showRepos ? 'Hide' : 'Show' }}
</button> </button>
<button class="btn btn-sm ml-1" ng-click="getGitHubRepositories()"> <button class="btn btn-sm ml-1" @click="getGitHubRepositories()">
<i class="fas fa-sync"></i> Refresh <i class="fas fa-sync"></i> Refresh
</button> </button>
</div> </div>
</div> </div>
<div ng-if="showRepos" style="margin-top: 20px"> <div style="margin-top: 20px" v-if="showRepos">
<div class="paper-section-eyebrow">GitHub repositories</div> <div class="paper-section-eyebrow">GitHub repositories</div>
<div class="paper-table w-100" style="margin-top: 10px;"> <div class="paper-table w-100" style="margin-top: 10px;">
<div class="paper-table-row" ng-repeat="repo in userInfo.repositories" style="grid-template-columns: 1fr 160px;"> <div class="paper-table-row" style="grid-template-columns: 1fr 160px;" v-for="(repo, index) in userInfo?.repositories">
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<span class="type-badge type-repo">Repo</span> <span class="type-badge type-repo">Repo</span>
<div class="anon-text"> <div class="anon-text">
<span class="repo-name" ng-bind="repo.name"></span> <span class="repo-name" v-text="repo?.name"></span>
</div> </div>
</div> </div>
<div class="cell-expires" role="cell"> <div class="cell-expires" role="cell">
<i class="fas fa-database"></i> {{::repo.size | humanFileSize}} <i class="fas fa-database"></i> {{ fmt?.humanFileSize(repo?.size) }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="admin-section-header" ng-if="userInfo && userInfo.isAdmin && user && user.username == userInfo.username"> <div class="admin-section-header" v-if="userInfo &amp;&amp; userInfo?.isAdmin &amp;&amp; user &amp;&amp; user?.username == userInfo?.username">
<h2><i class="fas fa-key"></i> API tokens</h2> <h2><i class="fas fa-key"></i> API tokens</h2>
<span class="section-count">{{tokens.length}}</span> <span class="section-count">{{ tokens?.length }}</span>
</div> </div>
<div ng-if="userInfo && userInfo.isAdmin && user && user.username == userInfo.username" class="user-detail-card"> <div class="user-detail-card" v-if="userInfo &amp;&amp; userInfo?.isAdmin &amp;&amp; user &amp;&amp; user?.username == userInfo?.username">
<p class="paper-page-lede">Personal API tokens for this admin account. Send as <code>Authorization: Bearer &lt;token&gt;</code> to authenticate without GitHub OAuth (useful for development).</p> <p class="paper-page-lede">Personal API tokens for this admin account. Send as <code>Authorization: Bearer &lt;token&gt;</code> to authenticate without GitHub OAuth (useful for development).</p>
<form ng-submit="createToken()" class="d-flex" style="gap: 8px; margin-bottom: 12px;"> <form class="d-flex" style="gap: 8px; margin-bottom: 12px;" v-form="viewState" @submit.prevent="submitForm($event, () =&gt; { createToken() })">
<input type="text" class="form-control" ng-model="tokenForm.name" placeholder="Token name (e.g. dev-laptop)" required /> <input type="text" class="form-control" placeholder="Token name (e.g. dev-laptop)" required v-field="{ state: viewState, set: value =&gt; { tokenForm.name = value }, value: tokenForm?.name, form: null, options: {} }">
<button type="submit" class="btn btn-primary"><i class="fas fa-plus"></i> Generate</button> <button type="submit" class="btn btn-primary"><i class="fas fa-plus"></i> Generate</button>
</form> </form>
<div ng-if="tokenForm.plaintext" class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert" v-if="tokenForm?.plaintext">
<strong>Copy this token now — it will not be shown again:</strong> <strong>Copy this token now — it will not be shown again:</strong>
<pre style="white-space: pre-wrap; word-break: break-all; margin: 8px 0 0; font-family: var(--font-mono); font-size: 0.85rem;">{{tokenForm.plaintext}}</pre> <pre style="white-space: pre-wrap; word-break: break-all; margin: 8px 0 0; font-family: var(--font-mono); font-size: 0.85rem;">{{ tokenForm?.plaintext }}</pre>
<button class="btn btn-sm" ng-click="tokenForm.plaintext = null">Dismiss</button> <button class="btn btn-sm" @click="tokenForm.plaintext = null">Dismiss</button>
</div> </div>
<div class="paper-table w-100" ng-if="tokens.length"> <div class="paper-table w-100" v-if="tokens?.length">
<div class="paper-table-head" role="row" style="grid-template-columns: 1fr 200px 200px 80px;"> <div class="paper-table-head" role="row" style="grid-template-columns: 1fr 200px 200px 80px;">
<div role="columnheader">Name</div> <div role="columnheader">Name</div>
<div role="columnheader">Created</div> <div role="columnheader">Created</div>
<div role="columnheader">Last used</div> <div role="columnheader">Last used</div>
<div role="columnheader" aria-label="Actions"></div> <div role="columnheader" aria-label="Actions"></div>
</div> </div>
<div class="paper-table-row" role="row" ng-repeat="t in tokens" style="grid-template-columns: 1fr 200px 200px 80px;"> <div class="paper-table-row" role="row" style="grid-template-columns: 1fr 200px 200px 80px;" v-for="(t, index) in tokens">
<div role="cell" ng-bind="t.name"></div> <div role="cell" v-text="t?.name"></div>
<div role="cell" ng-bind="t.createdAt | humanTime"></div> <div role="cell" v-text="fmt?.humanTime(t?.createdAt)"></div>
<div role="cell"><span ng-if="t.lastUsedAt">{{t.lastUsedAt | humanTime}}</span><span ng-if="!t.lastUsedAt" class="text-muted">never</span></div> <div role="cell"><span v-if="t?.lastUsedAt">{{ fmt?.humanTime(t?.lastUsedAt) }}</span><span class="text-muted" v-if="!t?.lastUsedAt">never</span></div>
<div role="cell"> <div role="cell">
<button class="btn btn-sm text-danger" ng-click="revokeToken(t)" title="Revoke"><i class="fas fa-trash-alt"></i></button> <button class="btn btn-sm text-danger" title="Revoke" @click="revokeToken(t)"><i class="fas fa-trash-alt"></i></button>
</div> </div>
</div> </div>
</div> </div>
<div class="paper-table-empty" ng-if="!tokens.length"> <div class="paper-table-empty" v-if="!tokens?.length">
<i class="fas fa-inbox"></i> <i class="fas fa-inbox"></i>
<span>No tokens yet.</span> <span>No tokens yet.</span>
</div> </div>
@@ -131,102 +131,90 @@
<div class="admin-section-header"> <div class="admin-section-header">
<h2><i class="fas fa-code-branch"></i> Anonymized repositories</h2> <h2><i class="fas fa-code-branch"></i> Anonymized repositories</h2>
<span class="section-count">{{repositories.length}}</span> <span class="section-count">{{ repositories?.length }}</span>
</div> </div>
<div class="admin-summary"> <div class="admin-summary">
<span class="summary-total">{{repositories.length | number}}</span> <span class="summary-total">{{ fmt?.number(repositories?.length) }}</span>
<span class="summary-pill ok" ng-class="{active: filters.status.ready === false}" ng-click="filters.status.ready = !filters.status.ready" title="Toggle ready filter">Ready <span class="count">{{statusCountFor('ready') | number}}</span></span> <span class="summary-pill ok" title="Toggle ready filter" :class="{active: filters?.status?.ready === false}" @click="filters.status.ready = !filters.status.ready">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></span>
<span class="summary-pill warn" ng-class="{active: filters.status.preparing === false}" ng-click="filters.status.preparing = !filters.status.preparing" title="Toggle preparing filter">Preparing <span class="count">{{statusCountFor('preparing') | number}}</span></span> <span class="summary-pill warn" title="Toggle preparing filter" :class="{active: filters?.status?.preparing === false}" @click="filters.status.preparing = !filters.status.preparing">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></span>
<span class="summary-pill error" ng-class="{active: filters.status.error === false}" ng-click="filters.status.error = !filters.status.error" title="Toggle errored filter">Errored <span class="count">{{statusCountFor('error') | number}}</span></span> <span class="summary-pill error" title="Toggle errored filter" :class="{active: filters?.status?.error === false}" @click="filters.status.error = !filters.status.error">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></span>
<span class="summary-pill" ng-class="{active: filters.status.expired === false}" ng-click="filters.status.expired = !filters.status.expired" title="Toggle expired filter">Expired <span class="count">{{statusCountFor('expired') | number}}</span></span> <span class="summary-pill" title="Toggle expired filter" :class="{active: filters?.status?.expired === false}" @click="filters.status.expired = !filters.status.expired">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></span>
<span class="summary-pill" ng-class="{active: filters.status.removed === false}" ng-click="filters.status.removed = !filters.status.removed" title="Toggle removed filter">Removed <span class="count">{{statusCountFor('removed') | number}}</span></span> <span class="summary-pill" title="Toggle removed filter" :class="{active: filters?.status?.removed === false}" @click="filters.status.removed = !filters.status.removed">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
</div> </div>
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8"> <form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
<div class="admin-filter-row"> <div class="admin-filter-row">
<div class="search-wrap"> <div class="search-wrap">
<input <input type="search" class="form-control" aria-label="Search repositories" placeholder="Search repoId, source repo, error message…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { search = value }, value: search, form: null, options: {} }">
type="search"
class="form-control"
aria-label="Search repositories"
placeholder="Search repoId, source repo, error message…"
autocomplete="off"
ng-model="search"
/>
</div> </div>
<span class="admin-filter-spacer"></span> <span class="admin-filter-spacer"></span>
<button class="btn btn-sm" type="button" ng-click="exportCsv()" title="Export current view to CSV"><i class="fas fa-file-csv"></i> Export</button> <button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
</div> </div>
</form> </form>
<div class="bulk-bar" ng-if="selectedCount() > 0"> <div class="bulk-bar" v-if="selectedCount() &gt; 0">
<span><strong>{{selectedCount()}}</strong> selected</span> <span><strong>{{ selectedCount() }}</strong> selected</span>
<button class="btn btn-sm" type="button" ng-click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button> <button class="btn btn-sm" type="button" @click="bulkRefresh()"><i class="fas fa-sync"></i> Force refresh</button>
<button class="btn btn-sm text-danger" type="button" ng-click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button> <button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
<button class="btn btn-sm" type="button" ng-click="clearSelection()">Clear</button> <button class="btn btn-sm" type="button" @click="clearSelection()">Clear</button>
</div> </div>
<div class="paper-table paper-table-repos has-bulk w-100" role="table" aria-label="Repositories"> <div class="paper-table paper-table-repos has-bulk w-100" role="table" aria-label="Repositories">
<div class="paper-table-head" role="row"> <div class="paper-table-head" role="row">
<div role="columnheader" style="width: 28px;"> <div role="columnheader" style="width: 28px;">
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" /> <input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
</div> </div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'source.repositoryName'}" ng-click="sortBy('source.repositoryName')">Repository <i class="fas" ng-class="sortIcon('source.repositoryName')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;source.repositoryName&#x27;}" @click="sortBy(&#x27;source.repositoryName&#x27;)">Repository <i class="fas" :class="sortIcon(&#x27;source.repositoryName&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'status'}" ng-click="sortBy('status')">Status <i class="fas" ng-class="sortIcon('status')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></i></span></div>
<div role="columnheader" class="num"><span class="sortable" ng-class="{active: query.sort == 'pageView'}" ng-click="sortBy('pageView')">Views <i class="fas" ng-class="sortIcon('pageView')"></i></span></div> <div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == &#x27;pageView&#x27;}" @click="sortBy(&#x27;pageView&#x27;)">Views <i class="fas" :class="sortIcon(&#x27;pageView&#x27;)"></i></span></div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'anonymizeDate'}" ng-click="sortBy('anonymizeDate')">Anonymized <i class="fas" ng-class="sortIcon('anonymizeDate')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;anonymizeDate&#x27;}" @click="sortBy(&#x27;anonymizeDate&#x27;)">Anonymized <i class="fas" :class="sortIcon(&#x27;anonymizeDate&#x27;)"></i></span></div>
<div role="columnheader" aria-label="Actions"></div> <div role="columnheader" aria-label="Actions"></div>
</div> </div>
<div <div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{&#x27;repo-inactive&#x27;: repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;, &#x27;repo-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;row-selected&#x27;: selected[repo?.repoId]}">
class="paper-table-row"
role="row"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'repo-error': repo.status == 'error', 'row-selected': selected[repo.repoId]}"
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
>
<div role="cell" style="width: 28px;"> <div role="cell" style="width: 28px;">
<input type="checkbox" ng-model="selected[repo.repoId]" aria-label="Select repository" /> <input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value =&gt; { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
</div> </div>
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<span class="type-badge type-repo">Repo</span> <span class="type-badge type-repo">Repo</span>
<div class="anon-text"> <div class="anon-text">
<a class="repo-name" target="_blank" ng-href="/r/{{repo.repoId}}" ng-bind="repo.repoId"></a> <a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId))"></a>
<div class="anon-sub"> <div class="anon-sub">
<a href="https://github.com/{{repo.source.fullName}}/" ng-bind="repo.source.fullName"></a><span ng-if="repo.options.update">&nbsp;&middot;&nbsp;<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.branch}}" ng-bind="repo.source.branch"></a></span><span ng-if="!repo.options.update">&nbsp;&middot;&nbsp;@<a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.commit}}" ng-bind="repo.source.commit.substring(0, 8)"></a></span><span ng-if="::repo.conference">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{repo.conference}}</span><span>&nbsp;&middot;&nbsp;{{::repo.size.storage | humanFileSize}}</span><span>&nbsp;&middot;&nbsp;{{::repo.options.terms.length | number}} terms</span> <a v-text="repo?.source?.fullName" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/&quot;)"></a><span v-if="repo?.options?.update">&nbsp;&middot;&nbsp;<a v-text="repo?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update">&nbsp;&middot;&nbsp;@<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.commit))"></a></span><span v-if="repo?.conference">&nbsp;&middot;&nbsp;<i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span>&nbsp;&middot;&nbsp;{{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span>&nbsp;&middot;&nbsp;{{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
</div> </div>
</div> </div>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<span class="status-line"> <span class="status-line">
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-preparing': repo.status == 'preparing'}"></span> <span class="status-dot" :class="{&#x27;status-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-preparing&#x27;: repo?.status == &#x27;preparing&#x27;}"></span>
<span ng-bind="repo.status | title"></span> <span v-text="fmt?.title(repo?.status)"></span>
</span> </span>
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage | statusMsg"></span> <span class="status-sub" v-if="repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="(repo?.statusMessage)"></span>
</div> </div>
<div class="cell-views num" role="cell" ng-bind="::repo.pageView || 0 | number"></div> <div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView || 0)"></div>
<div class="cell-expires" role="cell" ng-bind="repo.anonymizeDate | humanTime"></div> <div class="cell-expires" role="cell" v-text="fmt?.humanTime(repo?.anonymizeDate)"></div>
<div class="cell-actions" role="cell"> <div class="cell-actions" role="cell">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions"> <button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Actions">
<i class="fas fa-ellipsis-h" aria-hidden="true"></i> <i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/anonymize/{{repo.repoId}}"><i class="far fa-edit"></i> Edit</a> <a class="dropdown-item" :href="safeUrl(&quot;/anonymize/&quot; + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
<a class="dropdown-item" href="/r/{{repo.repoId}}/"><i class="fa fa-eye"></i> View repo</a> <a class="dropdown-item" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)"><i class="fa fa-eye"></i> View repo</a>
<a class="dropdown-item" href="/w/{{repo.repoId}}/" target="_self" ng-if="repo.options.page && repo.status == 'ready'"><i class="fas fa-globe"></i> View page</a> <a class="dropdown-item" target="_self" v-if="repo?.options?.page &amp;&amp; repo?.status == &#x27;ready&#x27;" :href="safeUrl(&quot;/w/&quot; + (repo?.repoId) + &quot;/&quot;)"><i class="fas fa-globe"></i> View page</a>
<a class="dropdown-item" href="#" ng-click="fetchGithubInfo(repo)"><i class="fab fa-github"></i> Live GitHub info</a> <a class="dropdown-item" href="#" @click.prevent="fetchGithubInfo(repo)"><i class="fab fa-github"></i> Live GitHub info</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" ng-show="repo.status == 'ready' || repo.status == 'error'" ng-click="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a> <a class="dropdown-item" href="#" v-show="repo?.status == &#x27;ready&#x27; || repo?.status == &#x27;error&#x27;" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
<a class="dropdown-item" href="#" ng-show="repo.status == 'removed'" ng-click="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a> <a class="dropdown-item" href="#" v-show="repo?.status == &#x27;removed&#x27;" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</a>
<a class="dropdown-item" href="#" ng-show="repo.statusMessage" ng-click="showStatusMessage(repo)"><i class="fas fa-exclamation-triangle"></i> View status message</a> <a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><i class="fas fa-exclamation-triangle"></i> View status message</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" ng-click="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a> <a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><i class="fas fa-broom"></i> Remove cache</a>
<a class="dropdown-item text-danger" href="#" ng-show="repo.status == 'ready'" ng-click="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a> <a class="dropdown-item text-danger" href="#" v-show="repo?.status == &#x27;ready&#x27;" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="paper-table-empty" ng-if="filteredRepositories.length == 0"> <div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
<i class="fas fa-inbox"></i> <i class="fas fa-inbox"></i>
<span>No repositories match the current filters.</span> <span>No repositories match the current filters.</span>
</div> </div>
+48 -53
View File
@@ -12,90 +12,85 @@
</nav> </nav>
<div class="admin-summary"> <div class="admin-summary">
<span class="summary-total">{{total >= 0 ? (total | number) : '…'}}</span> <span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
<span class="summary-pill ok" ng-class="{active: query.status == 'active'}" ng-click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1">Active <span class="count">{{statusCountFor('active') | number}}</span></span> <span class="summary-pill ok" :class="{active: query?.status == &#x27;active&#x27;}" @click="query.status = query.status == &#x27;active&#x27; ? &#x27;&#x27; : &#x27;active&#x27;; query.page = 1">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></span>
<span class="summary-pill error" ng-class="{active: query.status == 'banned'}" ng-click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1">Banned <span class="count">{{statusCountFor('banned') | number}}</span></span> <span class="summary-pill error" :class="{active: query?.status == &#x27;banned&#x27;}" @click="query.status = query.status == &#x27;banned&#x27; ? &#x27;&#x27; : &#x27;banned&#x27;; query.page = 1">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></span>
<span class="summary-pill" ng-class="{active: query.status == 'removed'}" ng-click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1">Removed <span class="count">{{statusCountFor('removed') | number}}</span></span> <span class="summary-pill" :class="{active: query?.status == &#x27;removed&#x27;}" @click="query.status = query.status == &#x27;removed&#x27; ? &#x27;&#x27; : &#x27;removed&#x27;; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
</div> </div>
<div class="alert alert-danger" ng-if="fetchError" style="margin: 8px 0;"> <div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
<i class="fas fa-exclamation-triangle"></i> {{fetchError}} <i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
</div> </div>
<form class="w-100 admin-filter-toolbar" aria-label="Users" accept-charset="UTF-8"> <form class="w-100 admin-filter-toolbar" aria-label="Users" accept-charset="UTF-8" v-form="viewState">
<div class="admin-filter-row"> <div class="admin-filter-row">
<div class="search-wrap"> <div class="search-wrap">
<input type="search" class="form-control" aria-label="Search users" placeholder="Search username or email…" autocomplete="off" ng-model="query.search" /> <input type="search" class="form-control" aria-label="Search users" placeholder="Search username or email…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<span class="admin-search-hint" ng-if="!query.search">/</span> <span class="admin-search-hint" v-if="!query?.search">/</span>
</div> </div>
<span class="admin-filter-inline"><label>Role</label> <span class="admin-filter-inline"><label>Role</label>
<select class="form-control form-control-sm" ng-model="query.role"><option value="">Any</option><option value="admin">Admin</option></select> <select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.role = value }, value: query?.role, form: null, options: {} }"><option value="">Any</option><option value="admin">Admin</option></select>
</span> </span>
<span class="admin-filter-spacer"></span> <span class="admin-filter-spacer"></span>
<button class="btn btn-sm" type="button" ng-click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button> <button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
<span class="admin-filter-inline" aria-label="Pagination"> <span class="admin-filter-inline" aria-label="Pagination">
<button class="btn btn-sm" type="button" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i></button> <button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 1"><i class="fas fa-chevron-left"></i></button>
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{query.page}}/{{totalPage || 1}}</span> <span style="font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);">{{ query?.page }}/{{ totalPage || 1 }}</span>
<button class="btn btn-sm" type="button" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage"><i class="fas fa-chevron-right"></i></button> <button class="btn btn-sm" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page &gt;= totalPage"><i class="fas fa-chevron-right"></i></button>
</span> </span>
</div> </div>
<div class="admin-filter-row" ng-if="chips.length"> <div class="admin-filter-row" v-if="chips?.length">
<div class="admin-active-chips"> <div class="admin-active-chips">
<span class="admin-active-chip" ng-repeat="chip in chips track by chip.key"> <span class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
<span class="key">{{chip.label}}</span> <span class="key">{{ chip?.label }}</span>
<span>{{chip.value}}</span> <span>{{ chip?.value }}</span>
<button type="button" ng-click="clearFilter(chip.key)"><i class="fas fa-times"></i></button> <button type="button" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
</span> </span>
</div> </div>
</div> </div>
</form> </form>
<div class="bulk-bar" ng-if="selectedCount() > 0"> <div class="bulk-bar" v-if="selectedCount() &gt; 0">
<span><strong>{{selectedCount()}}</strong> selected</span> <span><strong>{{ selectedCount() }}</strong> selected</span>
<button class="btn btn-sm text-danger" type="button" ng-click="bulkBan()"><i class="fas fa-ban"></i> Ban</button> <button class="btn btn-sm text-danger" type="button" @click="bulkBan()"><i class="fas fa-ban"></i> Ban</button>
<button class="btn btn-sm" type="button" ng-click="selected = {}; allSelected = false">Clear</button> <button class="btn btn-sm" type="button" @click="selected = {}; allSelected = false">Clear</button>
</div> </div>
<div class="paper-table w-100" role="table" aria-label="Users" style="--cols: 28px minmax(280px, 2.4fr) 100px 140px 140px 52px;"> <div class="paper-table w-100" role="table" aria-label="Users" style="--cols: 28px minmax(280px, 2.4fr) 100px 140px 140px 52px;">
<div class="paper-table-head admin-users-row" role="row"> <div class="paper-table-head admin-users-row" role="row">
<div role="columnheader" style="width: 28px;"> <div role="columnheader" style="width: 28px;">
<input type="checkbox" ng-click="selectAllOnPage()" ng-checked="allSelected" aria-label="Select all on page" /> <input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
</div> </div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'username'}" ng-click="sortBy('username')">User <i class="fas" ng-class="sortIcon('username')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;username&#x27;}" @click="sortBy(&#x27;username&#x27;)">User <i class="fas" :class="sortIcon(&#x27;username&#x27;)"></i></span></div>
<div role="columnheader" class="num">Repos</div> <div role="columnheader" class="num">Repos</div>
<div role="columnheader"><span class="sortable" ng-class="{active: query.sort == 'status'}" ng-click="sortBy('status')">Status <i class="fas" ng-class="sortIcon('status')"></i></span></div> <div role="columnheader"><span class="sortable" :class="{active: query?.sort == &#x27;status&#x27;}" @click="sortBy(&#x27;status&#x27;)">Status <i class="fas" :class="sortIcon(&#x27;status&#x27;)"></i></span></div>
<div role="columnheader">Role</div> <div role="columnheader">Role</div>
<div role="columnheader" aria-label="Actions"></div> <div role="columnheader" aria-label="Actions"></div>
</div> </div>
<div <div class="paper-table-row admin-users-row" role="row" v-for="(u, index) in filteredUsers" :class="{&#x27;row-selected&#x27;: selected[u?.username]}">
class="paper-table-row admin-users-row"
role="row"
ng-class="{'row-selected': selected[u.username]}"
ng-repeat="u in users | filter:userFiler | orderBy:orderBy as filteredUsers"
>
<div role="cell" style="width: 28px;"> <div role="cell" style="width: 28px;">
<input type="checkbox" ng-model="selected[u.username]" aria-label="Select user" /> <input type="checkbox" aria-label="Select user" v-field="{ state: viewState, set: value =&gt; { selected[u.username] = value }, value: selected[u?.username], form: null, options: {} }">
</div> </div>
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<img ng-src="{{u.photo}}" ng-if="u.photo" width="28" height="28" class="rounded-circle" style="flex-shrink:0;" /> <img width="28" height="28" class="rounded-circle" style="flex-shrink:0;" v-if="u?.photo" :src="safeUrl((u?.photo))">
<div class="anon-text"> <div class="anon-text">
<a class="repo-name" ng-href="/admin/users/{{u.username}}" ng-bind="u.username"></a> <a class="repo-name" v-text="u?.username" :href="safeUrl(&quot;/admin/users/&quot; + (u?.username))"></a>
<div class="anon-sub"> <div class="anon-sub">
<span ng-if="u.emails[0].email">{{u.emails[0].email}}</span><span ng-if="u.emails[0].email">&nbsp;&middot;&nbsp;</span><a href="https://github.com/{{u.username}}" target="_blank"><i class="fab fa-github"></i> {{u.username}}</a><span ng-if="u.dateOfEntry">&nbsp;&middot;&nbsp;Joined {{u.dateOfEntry | humanTime}}</span> <span v-if="u?.emails[0].email">{{ u?.emails[0].email }}</span><span v-if="u?.emails[0].email">&nbsp;&middot;&nbsp;</span><a target="_blank" :href="safeUrl(&quot;https://github.com/&quot; + (u?.username))"><i class="fab fa-github"></i> {{ u?.username }}</a><span v-if="u?.dateOfEntry">&nbsp;&middot;&nbsp;Joined {{ fmt?.humanTime(u?.dateOfEntry) }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="cell-views num" role="cell"> <div class="cell-views num" role="cell">
<a ng-href="/admin/?owner={{u.username}}" ng-bind="(u.repoCount || 0) | number" title="Show this user's repositories"></a> <a title="Show this user&#x27;s repositories" v-text="fmt?.number((u?.repoCount || 0))" :href="safeUrl(&quot;/admin/?owner=&quot; + (u?.username))"></a>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<span class="status-dot" ng-class="{'status-ready': u.status == 'active', 'status-removed': u.status == 'removed' || u.status == 'banned'}"></span> <span class="status-dot" :class="{&#x27;status-ready&#x27;: u?.status == &#x27;active&#x27;, &#x27;status-removed&#x27;: u?.status == &#x27;removed&#x27; || u?.status == &#x27;banned&#x27;}"></span>
<span ng-bind="u.status | title"></span> <span v-text="fmt?.title(u?.status)"></span>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<span ng-if="u.isAdmin" class="type-badge type-repo">Admin</span> <span class="type-badge type-repo" v-if="u?.isAdmin">Admin</span>
<span ng-if="!u.isAdmin" class="empty-dash">&mdash;</span> <span class="empty-dash" v-if="!u?.isAdmin">&mdash;</span>
</div> </div>
<div class="cell-actions" role="cell"> <div class="cell-actions" role="cell">
<div class="dropdown"> <div class="dropdown">
@@ -103,32 +98,32 @@
<i class="fas fa-ellipsis-h" aria-hidden="true"></i> <i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/admin/users/{{u.username}}"><i class="far fa-eye"></i> View details</a> <a class="dropdown-item" :href="safeUrl(&quot;/admin/users/&quot; + (u?.username))"><i class="far fa-eye"></i> View details</a>
<a class="dropdown-item" href="/admin/?owner={{u.username}}"><i class="fas fa-code-branch"></i> View repositories</a> <a class="dropdown-item" :href="safeUrl(&quot;/admin/?owner=&quot; + (u?.username))"><i class="fas fa-code-branch"></i> View repositories</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#" ng-show="u.status == 'active'" ng-click="banUser(u)"><i class="fas fa-ban"></i> Ban</a> <a class="dropdown-item text-danger" href="#" v-show="u?.status == &#x27;active&#x27;" @click.prevent="banUser(u)"><i class="fas fa-ban"></i> Ban</a>
<a class="dropdown-item" href="#" ng-show="u.status == 'removed' || u.status == 'banned'" ng-click="activateUser(u)"><i class="fas fa-check-circle"></i> Activate</a> <a class="dropdown-item" href="#" v-show="u?.status == &#x27;removed&#x27; || u?.status == &#x27;banned&#x27;" @click.prevent="activateUser(u)"><i class="fas fa-check-circle"></i> Activate</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="paper-table-empty" ng-if="filteredUsers.length == 0"> <div class="paper-table-empty" v-if="filteredUsers?.length == 0">
<i class="fas fa-inbox"></i> <i class="fas fa-inbox"></i>
<span>No users match the current filters.</span> <span>No users match the current filters.</span>
</div> </div>
</div> </div>
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;"> <div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
<span style="font-size: 12px; color: var(--ink-muted);">{{total | number}} results</span> <span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
<div class="pagination-compact" ng-if="totalPage > 1"> <div class="pagination-compact" v-if="totalPage &gt; 1">
<button class="btn btn-sm" ng-click="query.page = Math.max(1, query.page - 1)" ng-disabled="query.page <= 1"><i class="fas fa-chevron-left"></i> Previous</button> <button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page &lt;= 1"><i class="fas fa-chevron-left"></i> Previous</button>
<input type="number" class="form-control form-control-sm" ng-model="query.page" min="1" max="{{totalPage}}" style="width: 56px;" /> <input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value =&gt; { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
<span>of {{totalPage}}</span> <span>of {{ totalPage }}</span>
<button class="btn btn-sm" ng-click="query.page = Math.min(totalPage, query.page + 1)" ng-disabled="query.page >= totalPage">Next <i class="fas fa-chevron-right"></i></button> <button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page &gt;= totalPage">Next <i class="fas fa-chevron-right"></i></button>
</div> </div>
<span class="admin-filter-inline"> <span class="admin-filter-inline">
<label>Per page</label> <label>Per page</label>
<select class="form-control form-control-sm" ng-model="query.limit"><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100">100</option></select> <select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.limit = value }, value: query?.limit, form: null, options: {} }"><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100">100</option></select>
</span> </span>
</div> </div>
</div> </div>
+145 -190
View File
@@ -1,6 +1,6 @@
<div class="anonymize-page h-100"> <div class="anonymize-page h-100">
<!-- ===== STATE 1: No URL — centered input ===== --> <!-- ===== STATE 1: No URL — centered input ===== -->
<div class="anonymize-landing" ng-hide="sourceUrl"> <div class="anonymize-landing" v-show="!(sourceUrl)">
<div class="anonymize-landing-inner"> <div class="anonymize-landing-inner">
<div class="paper-crumbs">My work &nbsp;/&nbsp; <span class="here">New anonymization</span></div> <div class="paper-crumbs">My work &nbsp;/&nbsp; <span class="here">New anonymization</span></div>
<h1 class="paper-page-title">New <em>anonymization</em></h1> <h1 class="paper-page-title">New <em>anonymization</em></h1>
@@ -10,15 +10,7 @@
</p> </p>
<div class="form-group mt-4 mb-2"> <div class="form-group mt-4 mb-2">
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label> <label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
<input <input id="sourceUrl-landing" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value =&gt; { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { urlSelected() } }">
id="sourceUrl-landing"
type="text"
class="form-control form-control-lg"
ng-model="sourceUrl"
placeholder="https://github.com/owner/repository"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
ng-change="urlSelected()"
/>
</div> </div>
<small class="form-text" style="color: var(--ink-muted);"> <small class="form-text" style="color: var(--ink-muted);">
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted. Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
@@ -27,7 +19,7 @@
</div> </div>
<!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== --> <!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== -->
<div class="anonymize-workspace" ng-show="sourceUrl"> <div class="anonymize-workspace" v-show="sourceUrl">
<header class="anonymize-topbar"> <header class="anonymize-topbar">
<div class="anonymize-topbar-inner"> <div class="anonymize-topbar-inner">
<div class="paper-crumbs"> <div class="paper-crumbs">
@@ -36,11 +28,11 @@
</div> </div>
<div class="anonymize-topbar-head"> <div class="anonymize-topbar-head">
<h1 class="paper-page-title anonymize-topbar-title"> <h1 class="paper-page-title anonymize-topbar-title">
<span ng-if="!isUpdate">New <em>anonymization</em></span> <span v-if="!isUpdate">New <em>anonymization</em></span>
<span ng-if="isUpdate">Edit <em>anonymization</em></span> <span v-if="isUpdate">Edit <em>anonymization</em></span>
</h1> </h1>
<span class="type-badge" ng-show="detectedType" ng-class="{'type-repo': detectedType === 'repo', 'type-pr': detectedType === 'pr', 'type-gist': detectedType === 'gist'}"> <span class="type-badge" v-show="detectedType" :class="{&#x27;type-repo&#x27;: detectedType === &#x27;repo&#x27;, &#x27;type-pr&#x27;: detectedType === &#x27;pr&#x27;, &#x27;type-gist&#x27;: detectedType === &#x27;gist&#x27;}">
{{detectedType === 'repo' ? 'Repo' : detectedType === 'pr' ? 'PR' : 'Gist'}} {{ detectedType === 'repo' ? 'Repo' : detectedType === 'pr' ? 'PR' : 'Gist' }}
</span> </span>
</div> </div>
</div> </div>
@@ -49,47 +41,37 @@
<div class="anonymize-split"> <div class="anonymize-split">
<!-- Form column (left) --> <!-- Form column (left) -->
<div class="anonymize-form-col overflow-auto"> <div class="anonymize-form-col overflow-auto">
<form class="form needs-validation paper-settings-main" name="anonymize" novalidate> <form class="form needs-validation paper-settings-main" name="anonymize" novalidate v-form="viewState">
<section class="paper-settings-section"> <section class="paper-settings-section">
<div class="paper-section-eyebrow">Source</div> <div class="paper-section-eyebrow">Source</div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="sourceUrl">GitHub URL</label> <label class="paper-field-label" for="sourceUrl">GitHub URL</label>
<input <input type="text" class="form-control" name="sourceUrl" id="sourceUrl" :disabled="isUpdate &amp;&amp; detectedType !== &#x27;repo&#x27;" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value =&gt; { sourceUrl = value }, value: sourceUrl, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { urlSelected() } }" :class="{&#x27;is-invalid&#x27;: anonymize?.sourceUrl?.invalid}">
type="text" <div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.github">
class="form-control"
name="sourceUrl"
id="sourceUrl"
ng-class="{'is-invalid': anonymize.sourceUrl.$invalid}"
ng-model="sourceUrl"
placeholder="Paste a GitHub repo or pull request URL"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
ng-change="urlSelected()"
/>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.github">
Please provide a valid GitHub URL. Please provide a valid GitHub URL.
</div> </div>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.access"> <div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.access">
Not accessible. The organization may restrict access. Not accessible. The organization may restrict access.
</div> </div>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.missing"> <div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.missing">
Does not exist or is not accessible. Does not exist or is not accessible.
</div> </div>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.used"> <div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.used">
Already anonymized. Already anonymized.
</div> </div>
</div> </div>
<div ng-show="detectedType === 'repo'" class="form-grid-2"> <div class="form-grid-2" v-show="detectedType === &#x27;repo&#x27;">
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="branch">Branch</label> <label class="paper-field-label" for="branch">Branch</label>
<div class="input-group"> <div class="input-group">
<select class="form-control" id="branch" name="branch" ng-model="source.branch"> <select class="form-control" id="branch" name="branch" v-field="{ state: viewState, set: value =&gt; { source.branch = value }, value: source?.branch, form: &quot;anonymize&quot;, options: {} }">
<option ng-repeat="b in branches" ng-bind="b.name" value="{{b.name}}"></option> <option v-for="(b, index) in branches" v-text="b?.name" :value="(b?.name)"></option>
</select> </select>
<div class="input-group-append"> <div class="input-group-append">
<button class="btn" type="button" ng-click="getBranches(true)" title="Refresh" data-toggle="tooltip" data-placement="bottom"> <button class="btn" type="button" title="Refresh" data-toggle="tooltip" data-placement="bottom" @click="getBranches(true)">
<i class="fas fa-sync" aria-hidden="true"></i> <i class="fas fa-sync" aria-hidden="true"></i>
</button> </button>
</div> </div>
@@ -98,250 +80,241 @@
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="commit">Commit</label> <label class="paper-field-label" for="commit">Commit</label>
<input class="form-control" id="commit" name="commit" pattern="[a-fA-Z0-9]{6,}" ng-model="source.commit" required ng-class="{'is-invalid': anonymize.commit.$invalid}" /> <input class="form-control" id="commit" :disabled="options.update" name="commit" pattern="[a-fA-Z0-9]{6,}" required v-field="{ state: viewState, set: value =&gt; { source.commit = value }, value: source?.commit, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: anonymize?.commit?.invalid}">
<div class="invalid-feedback" ng-show="anonymize.commit.$error.pattern || anonymize.commit.$error.required"> <div class="invalid-feedback" v-show="anonymize?.commit?.errors?.pattern || anonymize?.commit?.errors?.required">
The commit SHA is not valid. The commit SHA is not valid.
</div> </div>
<div class="invalid-feedback" ng-show="anonymize.commit.$error.exists"> <div class="invalid-feedback" v-show="anonymize?.commit?.errors?.exists">
This commit no longer exists in the repository. Click refresh to get the latest. This commit no longer exists in the repository. Click refresh to get the latest.
</div> </div>
</div> </div>
</div> </div>
<div class="form-check" ng-show="detectedType"> <div class="form-check" v-show="detectedType">
<input class="form-check-input" type="checkbox" id="update" name="update" ng-model="options.update" /> <input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value =&gt; { options.update = value }, value: options?.update, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="update">Auto-update from GitHub</label> <label class="form-check-label" for="update">Auto-update from GitHub</label>
<small class="form-text text-muted">Follow the branch and pull the latest commit automatically, at most once an hour.</small> <small class="form-text text-muted">Follow the branch and pull the latest commit automatically, at most once an hour.</small>
</div> </div>
</section> </section>
<section class="paper-settings-section" ng-show="detectedType"> <section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Identity</div> <div class="paper-section-eyebrow">Identity</div>
<div class="form-group" ng-show="detectedType === 'repo'"> <div class="form-group" v-show="detectedType === &#x27;repo&#x27;">
<label class="paper-field-label" for="repoId">Anonymized repository ID</label> <label class="paper-field-label" for="repoId">Anonymized repository ID</label>
<input type="text" class="form-control" name="repoId" id="repoId" ng-class="{'is-invalid': anonymize.repoId.$invalid}" ng-model="repoId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" /> <input type="text" class="form-control" name="repoId" id="repoId" :disabled="isUpdate" v-field="{ state: viewState, set: value =&gt; { repoId = value }, value: repoId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.repoId?.invalid}">
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/r/{{repoId}}</code>.</small> <small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/r/{{ repoId }}</code>.</small>
<div class="invalid-feedback" ng-show="anonymize.repoId.$error.format">ID can only contain letters and numbers.</div> <div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" ng-show="anonymize.repoId.$error.used">{{repoId}} is already used.</div> <div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.used">{{ repoId }} is already used.</div>
</div> </div>
<div class="form-group" ng-show="detectedType === 'pr'"> <div class="form-group" v-show="detectedType === &#x27;pr&#x27;">
<label class="paper-field-label" for="pullRequestId">Anonymized pull request ID</label> <label class="paper-field-label" for="pullRequestId">Anonymized pull request ID</label>
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" ng-class="{'is-invalid': anonymize.pullRequestId.$invalid}" ng-model="pullRequestId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" /> <input type="text" class="form-control" name="pullRequestId" id="pullRequestId" :disabled="isUpdate" v-field="{ state: viewState, set: value =&gt; { pullRequestId = value }, value: pullRequestId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.pullRequestId?.invalid}">
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/pr/{{pullRequestId}}</code>.</small> <small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/pr/{{ pullRequestId }}</code>.</small>
<div class="invalid-feedback" ng-show="anonymize.pullRequestId.$error.format">ID can only contain letters and numbers.</div> <div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" ng-show="anonymize.pullRequestId.$error.used">{{pullRequestId}} is already used.</div> <div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">{{ pullRequestId }} is already used.</div>
</div> </div>
<div class="form-group" ng-show="detectedType === 'gist'"> <div class="form-group" v-show="detectedType === &#x27;gist&#x27;">
<label class="paper-field-label" for="gistId">Anonymized gist ID</label> <label class="paper-field-label" for="gistId">Anonymized gist ID</label>
<input type="text" class="form-control" name="gistId" id="gistId" ng-class="{'is-invalid': anonymize.gistId.$invalid}" ng-model="gistId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" /> <input type="text" class="form-control" name="gistId" id="gistId" :disabled="isUpdate" v-field="{ state: viewState, set: value =&gt; { gistId = value }, value: gistId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.gistId?.invalid}">
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/gist/{{gistId}}</code>.</small> <small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/gist/{{ gistId }}</code>.</small>
<div class="invalid-feedback" ng-show="anonymize.gistId.$error.format">ID can only contain letters and numbers.</div> <div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" ng-show="anonymize.gistId.$error.used">{{gistId}} is already used.</div> <div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.used">{{ gistId }} is already used.</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="conference">Conference <span class="paper-optional">(optional)</span></label> <label class="paper-field-label" for="conference">Conference <span class="paper-optional">(optional)</span></label>
<input class="form-control" id="conference" name="conference" ng-model="conference" ng-model-options="{ debounce: { default: 800, blur: 0 }, updateOn: 'default blur' }" ng-class="{'is-invalid': anonymize.conference.$invalid}" /> <input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value =&gt; { conference = value }, value: conference, form: &quot;anonymize&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.conference?.invalid}">
<small class="form-text text-muted" ng-show="conference_data"> <small class="form-text text-muted" v-show="conference_data">
<a ng-href="{{conference_data.url}}" target="_blank">{{conference_data.name}}</a> expires {{conference_data.endDate | date}}. <a target="_blank" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a> expires {{ fmt?.date(conference_data?.endDate) }}.
</small> </small>
<div class="invalid-feedback" ng-show="anonymize.conference.$error.activated">The conference is not activated.</div> <div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">The conference is not activated.</div>
<small class="form-text text-muted" ng-show="!conference_data">Link to a conference to apply its shared defaults.</small> <small class="form-text text-muted" v-show="!conference_data">Link to a conference to apply its shared defaults.</small>
</div> </div>
</section> </section>
<section class="paper-settings-section" ng-show="detectedType"> <section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Anonymization</div> <div class="paper-section-eyebrow">Anonymization</div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="terms">Terms to redact</label> <label class="paper-field-label" for="terms">Terms to redact</label>
<textarea class="form-control" id="terms" name="terms" rows="4" ng-model="terms" ng-model-options="{ debounce: 250 }" ng-class="{'is-invalid': anonymize.terms.$invalid}"></textarea> <textarea class="form-control" id="terms" name="terms" rows="4" v-field="{ state: viewState, set: value =&gt; { terms = value }, value: terms, form: &quot;anonymize&quot;, options: { debounce: 250 } }" :class="{&#x27;is-invalid&#x27;: anonymize?.terms?.invalid}"></textarea>
<small class="form-text text-muted">One term per line (regex allowed). Replaced by <code>{{site_options.ANONYMIZATION_MASK}}-[N]</code>, or use <code>term=&gt;replacement</code> to pick your own (e.g. <code>Anonymous=&gt;ABC</code>).</small> <small class="form-text text-muted">One term per line (regex allowed). Replaced by <code>{{ site_options?.ANONYMIZATION_MASK }}-[N]</code>, or use <code>term=&gt;replacement</code> to pick your own (e.g. <code>Anonymous=&gt;ABC</code>).</small>
<div class="warning-feedback" ng-show="termsRegexWarning">Regex characters detected. Escape them if unintentional.</div> <div class="warning-feedback" v-show="termsRegexWarning">Regex characters detected. Escape them if unintentional.</div>
<div class="invalid-feedback" ng-show="anonymize.terms.$error.format">Terms are in an invalid format.</div> <div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">Terms are in an invalid format.</div>
</div> </div>
</section> </section>
<section class="paper-settings-section" ng-show="detectedType"> <section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Display</div> <div class="paper-section-eyebrow">Display</div>
<div ng-show="detectedType === 'repo'"> <div v-show="detectedType === &#x27;repo&#x27;">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="link" name="link" ng-model="options.link" /> <input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value =&gt; { options.link = value }, value: options?.link, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="link">Keep links</label> <label class="form-check-label" for="link">Keep links</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="image" name="image" ng-model="options.image" /> <input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value =&gt; { options.image = value }, value: options?.image, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="image">Display images</label> <label class="form-check-label" for="image">Display images</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" ng-model="options.pdf" /> <input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value =&gt; { options.pdf = value }, value: options?.pdf, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="pdf">Display PDFs</label> <label class="form-check-label" for="pdf">Display PDFs</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" ng-model="options.notebook" /> <input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value =&gt; { options.notebook = value }, value: options?.notebook, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="notebook">Display Notebooks</label> <label class="form-check-label" for="notebook">Display Notebooks</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="page" name="page" ng-model="options.page" ng-disabled="!details.hasPage || (details.pageSource && details.pageSource.branch !== source.branch)" /> <input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value =&gt; { options.page = value }, value: options?.page, form: &quot;anonymize&quot;, options: {} }" :disabled="!details?.hasPage || (details?.pageSource &amp;&amp; details?.pageSource?.branch !== source?.branch)">
<label class="form-check-label" for="page">GitHub Pages</label> <label class="form-check-label" for="page">GitHub Pages</label>
<small class="form-text text-muted d-block" ng-show="!details.hasPage"> <small class="form-text text-muted d-block" v-show="!details?.hasPage">
{{ 'WARNINGS.page_not_enabled_on_repo' | translate }} {{ fmt?.translate('WARNINGS.page_not_enabled_on_repo') }}
</small> </small>
<small class="form-text text-muted d-block" ng-show="details.hasPage && details.pageSource && details.pageSource.branch !== source.branch"> <small class="form-text text-muted d-block" v-show="details?.hasPage &amp;&amp; details?.pageSource &amp;&amp; details?.pageSource?.branch !== source?.branch">
{{ 'WARNINGS.page_branch_mismatch' | translate:{ pageBranch: details.pageSource.branch, selectedBranch: source.branch } }} {{ fmt?.translate('WARNINGS.page_branch_mismatch', { pageBranch: details?.pageSource?.branch, selectedBranch: source?.branch }) }}
</small> </small>
</div> </div>
</div> </div>
<div ng-show="detectedType === 'gist'"> <div v-show="detectedType === &#x27;gist&#x27;">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" ng-model="options.title" /> <input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" v-field="{ state: viewState, set: value =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="title-gist">Gist description</label> <label class="form-check-label" for="title-gist">Gist description</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" ng-model="options.content" /> <input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" v-field="{ state: viewState, set: value =&gt; { options.content = value }, value: options?.content, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="content-gist">File contents</label> <label class="form-check-label" for="content-gist">File contents</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" ng-model="options.comments" /> <input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" v-field="{ state: viewState, set: value =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="comments-gist">Comments</label> <label class="form-check-label" for="comments-gist">Comments</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" ng-model="options.username" /> <input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" v-field="{ state: viewState, set: value =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="username-gist">Usernames</label> <label class="form-check-label" for="username-gist">Usernames</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" ng-model="options.date" /> <input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" v-field="{ state: viewState, set: value =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="date-gist">Dates</label> <label class="form-check-label" for="date-gist">Dates</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" ng-model="options.origin" /> <input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" v-field="{ state: viewState, set: value =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="origin-gist">Source gist ID</label> <label class="form-check-label" for="origin-gist">Source gist ID</label>
</div> </div>
</div> </div>
<div ng-show="detectedType === 'pr'"> <div v-show="detectedType === &#x27;pr&#x27;">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="title" name="title" ng-model="options.title" /> <input class="form-check-input" type="checkbox" id="title" name="title" v-field="{ state: viewState, set: value =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="title">PR title</label> <label class="form-check-label" for="title">PR title</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="body" name="body" ng-model="options.body" /> <input class="form-check-input" type="checkbox" id="body" name="body" v-field="{ state: viewState, set: value =&gt; { options.body = value }, value: options?.body, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="body">PR body</label> <label class="form-check-label" for="body">PR body</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="diff" name="diff" ng-model="options.diff" /> <input class="form-check-input" type="checkbox" id="diff" name="diff" v-field="{ state: viewState, set: value =&gt; { options.diff = value }, value: options?.diff, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="diff">Diff</label> <label class="form-check-label" for="diff">Diff</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="comments" name="comments" ng-model="options.comments" /> <input class="form-check-input" type="checkbox" id="comments" name="comments" v-field="{ state: viewState, set: value =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="comments">Comments</label> <label class="form-check-label" for="comments">Comments</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="username" name="username" ng-model="options.username" /> <input class="form-check-input" type="checkbox" id="username" name="username" v-field="{ state: viewState, set: value =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="username">Usernames</label> <label class="form-check-label" for="username">Usernames</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="date" name="date" ng-model="options.date" /> <input class="form-check-input" type="checkbox" id="date" name="date" v-field="{ state: viewState, set: value =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="date">Dates</label> <label class="form-check-label" for="date">Dates</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="origin" name="origin" ng-model="options.origin" /> <input class="form-check-input" type="checkbox" id="origin" name="origin" v-field="{ state: viewState, set: value =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="origin">Project name</label> <label class="form-check-label" for="origin">Project name</label>
</div> </div>
</div> </div>
</section> </section>
<section class="paper-settings-section" ng-show="detectedType"> <section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Expiration</div> <div class="paper-section-eyebrow">Expiration</div>
<div class="form-grid-2"> <div class="form-grid-2">
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="expiration">Strategy</label> <label class="paper-field-label" for="expiration">Strategy</label>
<select class="form-control" id="expiration" name="expiration" ng-model="options.expirationMode"> <select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;anonymize&quot;, options: {} }">
<option value="redirect">Redirect to GitHub when expired</option> <option value="redirect">Redirect to GitHub when expired</option>
<option value="remove" selected>Remove when expired</option> <option value="remove" selected>Remove when expired</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="expirationDate">Expiration date</label> <label class="paper-field-label" for="expirationDate">Expiration date</label>
<input class="form-control" type="date" name="expirationDate" id="expirationDate" ng-model="options.expirationDate" ng-class="{'is-invalid': anonymize.expirationDate.$invalid}" min="{{minExpirationDate}}" max="{{maxExpirationDate}}" required /> <input class="form-control" type="date" name="expirationDate" id="expirationDate" required v-field="{ state: viewState, set: value =&gt; { options.expirationDate = value }, value: options?.expirationDate, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: anonymize?.expirationDate?.invalid}" :min="(minExpirationDate)" :max="(maxExpirationDate)">
<div class="invalid-feedback" ng-show="anonymize.expirationDate.$error.min">Pick a date in the future.</div> <div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.min">Pick a date in the future.</div>
<div class="invalid-feedback" ng-show="anonymize.expirationDate.$error.max">Pick a date on or before {{maxExpirationDate | date}}.</div> <div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.max">Pick a date on or before {{ fmt?.date(maxExpirationDate) }}.</div>
<div class="invalid-feedback" ng-show="anonymize.expirationDate.$error.required || anonymize.expirationDate.$error.date">Enter a valid expiration date.</div> <div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.required || anonymize?.expirationDate?.errors?.date">Enter a valid expiration date.</div>
</div> </div>
</div> </div>
<small class="form-text text-muted" ng-show="options.expirationMode=='remove' && options.expirationDate">After {{options.expirationDate | date}}, the content will be removed.</small> <small class="form-text text-muted" v-show="options?.expirationMode==&#x27;remove&#x27; &amp;&amp; options?.expirationDate">After {{ fmt?.date(options?.expirationDate) }}, the content will be removed.</small>
<small class="form-text text-muted" ng-show="options.expirationMode=='redirect' && options.expirationDate">After {{options.expirationDate | date}}, visitors will be redirected to GitHub.</small> <small class="form-text text-muted" v-show="options?.expirationMode==&#x27;redirect&#x27; &amp;&amp; options?.expirationDate">After {{ fmt?.date(options?.expirationDate) }}, visitors will be redirected to GitHub.</small>
</section> </section>
<section class="paper-settings-section" ng-show="isUpdate && detectedType === 'repo'"> <section class="paper-settings-section" v-show="isUpdate &amp;&amp; detectedType === &#x27;repo&#x27;">
<div class="paper-section-eyebrow">Co-authors</div> <div class="paper-section-eyebrow">Co-authors</div>
<p class="form-text text-muted" style="margin-bottom: 8px;"> <p class="form-text text-muted" style="margin-bottom: 8px;">
Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors. Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors.
</p> </p>
<div class="form-group" ng-show="role === 'owner' || role === 'admin'"> <div class="form-group" v-show="role === &#x27;owner&#x27; || role === &#x27;admin&#x27;">
<label class="paper-field-label" for="coauthorSearch">Add a GitHub user</label> <label class="paper-field-label" for="coauthorSearch">Add a GitHub user</label>
<div style="position: relative;"> <div style="position: relative;">
<input <input type="text" id="coauthorSearch" class="form-control" placeholder="Search GitHub username…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { coauthorSearch = value }, value: coauthorSearch, form: &quot;anonymize&quot;, options: { debounce: 300 }, change: () =&gt; { searchCoauthors() } }">
type="text" <div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" v-show="coauthorResults?.length &gt; 0">
id="coauthorSearch" <a href="#" class="dropdown-item d-flex align-items-center" v-for="(u, index) in coauthorResults" @click.prevent="addCoauthor(u, $event)">
class="form-control" <img alt="" style="width: 22px; height: 22px; border-radius: 50%; margin-right: 8px;" :src="safeUrl((u?.photo))">
placeholder="Search GitHub username" <span v-text="u?.username"></span>
ng-model="coauthorSearch"
ng-change="searchCoauthors()"
ng-model-options="{ debounce: 300 }"
autocomplete="off"
/>
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" ng-show="coauthorResults.length > 0">
<a href="#" class="dropdown-item d-flex align-items-center" ng-repeat="u in coauthorResults" ng-click="addCoauthor(u, $event)">
<img ng-src="{{u.photo}}" alt="" style="width: 22px; height: 22px; border-radius: 50%; margin-right: 8px;" />
<span ng-bind="u.username"></span>
</a> </a>
</div> </div>
</div> </div>
<small class="form-text text-muted" ng-show="coauthorError" ng-bind="coauthorError"></small> <small class="form-text text-muted" v-show="coauthorError" v-text="coauthorError"></small>
</div> </div>
<div class="coauthor-list"> <div class="coauthor-list">
<div class="coauthor-row d-flex align-items-center" ng-repeat="c in coauthors" style="padding: 6px 0; gap: 8px;"> <div class="coauthor-row d-flex align-items-center" style="padding: 6px 0; gap: 8px;" v-for="(c, index) in coauthors">
<img ng-src="{{c.photo}}" alt="" style="width: 24px; height: 24px; border-radius: 50%;" ng-if="c.photo" /> <img alt="" style="width: 24px; height: 24px; border-radius: 50%;" v-if="c?.photo" :src="safeUrl((c?.photo))">
<a ng-href="https://github.com/{{c.username}}" target="_blank" ng-bind="c.username"></a> <a target="_blank" v-text="c?.username" :href="safeUrl(&quot;https://github.com/&quot; + (c?.username))"></a>
<span class="type-badge type-coauthor">Co-author</span> <span class="type-badge type-coauthor">Co-author</span>
<button type="button" class="btn btn-sm" ng-click="removeCoauthor(c)" ng-show="role === 'owner' || role === 'admin'" style="margin-left: auto;" title="Remove co-author"> <button type="button" class="btn btn-sm" style="margin-left: auto;" title="Remove co-author" v-show="role === &#x27;owner&#x27; || role === &#x27;admin&#x27;" @click="removeCoauthor(c)">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="form-text text-muted" ng-show="!coauthors || coauthors.length === 0"> <div class="form-text text-muted" v-show="!coauthors || coauthors?.length === 0">
No co-authors yet. No co-authors yet.
</div> </div>
</div> </div>
</section> </section>
<div class="alert alert-danger" role="alert" ng-if="error" ng-bind="error"></div> <div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
<div class="anonymize-submit-bar" ng-show="detectedType"> <div class="anonymize-submit-bar" v-show="detectedType">
<button type="submit" class="btn btn-ink" ng-click="anonymizeRepo($event)" ng-if="detectedType === 'repo' && !isUpdate"> <button type="submit" class="btn btn-ink" v-if="detectedType === &#x27;repo&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeRepo($event) })">
<i class="fas fa-user-secret mr-1"></i> Anonymize Repository <i class="fas fa-user-secret mr-1"></i> Anonymize Repository
</button> </button>
<button type="submit" class="btn btn-ink" ng-click="anonymizeRepo($event)" ng-if="detectedType === 'repo' && isUpdate"> <button type="submit" class="btn btn-ink" v-if="detectedType === &#x27;repo&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeRepo($event) })">
<i class="fas fa-save mr-1"></i> Update Repository <i class="fas fa-save mr-1"></i> Update Repository
</button> </button>
<button type="submit" class="btn btn-ink" ng-click="anonymizePullRequest($event)" ng-if="detectedType === 'pr' && !isUpdate"> <button type="submit" class="btn btn-ink" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizePullRequest($event) })">
<i class="fas fa-user-secret mr-1"></i> Anonymize Pull Request <i class="fas fa-user-secret mr-1"></i> Anonymize Pull Request
</button> </button>
<button type="submit" class="btn btn-ink" ng-click="anonymizePullRequest($event)" ng-if="detectedType === 'pr' && isUpdate"> <button type="submit" class="btn btn-ink" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizePullRequest($event) })">
<i class="fas fa-save mr-1"></i> Update Pull Request <i class="fas fa-save mr-1"></i> Update Pull Request
</button> </button>
<button type="submit" class="btn btn-ink" ng-click="anonymizeGist($event)" ng-if="detectedType === 'gist' && !isUpdate"> <button type="submit" class="btn btn-ink" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeGist($event) })">
<i class="fas fa-user-secret mr-1"></i> Anonymize Gist <i class="fas fa-user-secret mr-1"></i> Anonymize Gist
</button> </button>
<button type="submit" class="btn btn-ink" ng-click="anonymizeGist($event)" ng-if="detectedType === 'gist' && isUpdate"> <button type="submit" class="btn btn-ink" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeGist($event) })">
<i class="fas fa-save mr-1"></i> Update Gist <i class="fas fa-save mr-1"></i> Update Gist
</button> </button>
</div> </div>
@@ -349,18 +322,15 @@
</div> </div>
<!-- Preview column (right) --> <!-- Preview column (right) -->
<div <div class="anonymize-preview-col" v-if="detectedType === &#x27;repo&#x27; &amp;&amp; html_readme">
class="anonymize-preview-col"
ng-if="detectedType === 'repo' && html_readme"
>
<div class="anonymize-preview-head"> <div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span> <span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">README with redactions applied</span> <span class="anonymize-preview-sub">README with redactions applied</span>
</div> </div>
<div class="anonymize-preview-body markdown-body body" ng-bind-html="html_readme"></div> <div class="anonymize-preview-body markdown-body body" v-html="sanitize(html_readme)"></div>
</div> </div>
<div class="anonymize-preview-col" ng-if="detectedType === 'gist' && details"> <div class="anonymize-preview-col" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; details">
<div class="anonymize-preview-head"> <div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span> <span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">Gist with redactions applied</span> <span class="anonymize-preview-sub">Gist with redactions applied</span>
@@ -368,38 +338,38 @@
<div class="anonymize-preview-body"> <div class="anonymize-preview-body">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap"> <div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1"> <h2 class="pr-title mb-1">
<span ng-if="options.title">{{anonymizeGistContent(details.gist.description) || 'Untitled gist'}}</span> <span v-if="options?.title">{{ anonymizeGistContent(details?.gist?.description) || 'Untitled gist' }}</span>
<span class="badge" ng-class="{'badge-success': details.gist.isPublic, 'badge-secondary': !details.gist.isPublic}"> <span class="badge" :class="{&#x27;badge-success&#x27;: details?.gist?.isPublic, &#x27;badge-secondary&#x27;: !details?.gist?.isPublic}">
{{details.gist.isPublic ? 'public' : 'secret'}} {{ details?.gist?.isPublic ? 'public' : 'secret' }}
</span> </span>
</h2> </h2>
<small ng-bind="details.gist.updatedDate | date" ng-if="options.date"></small> <small v-if="options?.date" v-text="fmt?.date(details?.gist?.updatedDate)"></small>
</div> </div>
<small ng-if="options.origin">Gist ID: {{details.source.gistId}}</small> <small v-if="options?.origin">Gist ID: {{ details?.source?.gistId }}</small>
<small ng-if="options.username && details.gist.ownerLogin">By @{{anonymizeGistContent(details.gist.ownerLogin)}}</small> <small v-if="options?.username &amp;&amp; details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
<div ng-if="options.content && previewGistFiles.length"> <div v-if="options?.content &amp;&amp; previewGistFiles?.length">
<ul class="pr-comments mt-3"> <ul class="pr-comments mt-3">
<li class="pr-comment" ng-repeat="file in previewGistFiles"> <li class="pr-comment" v-for="(file, index) in previewGistFiles">
<div class="pr-comment-head"> <div class="pr-comment-head">
<strong ng-bind="file.filename"></strong> <strong v-text="file?.filename"></strong>
<span class="pr-comment-date" ng-if="file.language">{{file.language}}</span> <span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
</div> </div>
<gist-file file="file" terms="terms" options="options"></gist-file> <gist-file :file="file" :terms="terms" :options="options"></gist-file>
</li> </li>
</ul> </ul>
</div> </div>
<div ng-if="options.comments && details.gist.comments && details.gist.comments.length"> <div v-if="options?.comments &amp;&amp; details?.gist?.comments &amp;&amp; details?.gist?.comments?.length">
<h3 class="paper-section-eyebrow mt-3">Comments</h3> <h3 class="paper-section-eyebrow mt-3">Comments</h3>
<ul class="pr-comments"> <ul class="pr-comments">
<li class="pr-comment" ng-repeat="comment in details.gist.comments"> <li class="pr-comment" v-for="(comment, index) in details?.gist?.comments">
<div class="pr-comment-head"> <div class="pr-comment-head">
<span class="pr-comment-author" ng-if="options.username"> <span class="pr-comment-author" v-if="options?.username">
<i class="far fa-user"></i> @<span ng-bind="anonymizeGistContent(comment.author)"></span> <i class="far fa-user"></i> @<span v-text="anonymizeGistContent(comment?.author)"></span>
</span> </span>
<span class="pr-comment-date" ng-if="options.date" ng-bind="comment.updatedDate | date"></span> <span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
</div> </div>
<div class="pr-comment-body" ng-if="options.body"> <div class="pr-comment-body" v-if="options?.body">
<markdown content="anonymizeGistContent(comment.body)" options="options" terms="terms"></markdown> <markdown :content="anonymizeGistContent(comment?.body)" :options="options" :terms="terms"></markdown>
</div> </div>
</li> </li>
</ul> </ul>
@@ -407,8 +377,7 @@
</div> </div>
</div> </div>
<div class="anonymize-preview-col" ng-if="detectedType === 'pr' && details" <div class="anonymize-preview-col" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; details">
ng-init="prTabState = { active: options.diff ? 'diff' : 'comments' }">
<div class="anonymize-preview-head"> <div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span> <span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">Pull request with redactions applied</span> <span class="anonymize-preview-sub">Pull request with redactions applied</span>
@@ -416,55 +385,41 @@
<div class="anonymize-preview-body"> <div class="anonymize-preview-body">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap"> <div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1"> <h2 class="pr-title mb-1">
<span ng-if="options.title">{{anonymizePrContent(details.pullRequest.title)}}</span> <span v-if="options?.title">{{ anonymizePrContent(details?.pullRequest?.title) }}</span>
<span class="badge" ng-class="{'badge-success':details.pullRequest.merged, 'badge-warning':details.pullRequest.state=='open', 'badge-danger':details.pullRequest.state=='closed' && !details.pullRequest.merged}"> <span class="badge" :class="{&#x27;badge-success&#x27;:details?.pullRequest?.merged, &#x27;badge-warning&#x27;:details?.pullRequest?.state==&#x27;open&#x27;, &#x27;badge-danger&#x27;:details?.pullRequest?.state==&#x27;closed&#x27; &amp;&amp; !details?.pullRequest?.merged}">
{{details.pullRequest.merged ? "merged" : details.pullRequest.state | title}} {{ fmt?.title(details?.pullRequest?.merged ? "merged" : details?.pullRequest?.state) }}
</span> </span>
</h2> </h2>
<small ng-bind="details.pullRequest.updatedDate | date" ng-if="options.date"></small> <small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
</div> </div>
<small ng-if="options.origin">Pull Request on {{details.pullRequest.baseRepositoryFullName}}</small> <small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
<div class="pr-body shadow-sm p-3 mb-4 rounded" style="background: var(--paper-bg-alt)" ng-if="options.body"> <div class="pr-body shadow-sm p-3 mb-4 rounded" style="background: var(--paper-bg-alt)" v-if="options?.body">
<markdown content="anonymizePrContent(details.pullRequest.body)" options="options" terms="terms"></markdown> <markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
</div> </div>
<nav class="paper-tabs" ng-if="options.diff || options.comments" role="tablist"> <nav class="paper-tabs" role="tablist" v-if="options?.diff || options?.comments">
<button <button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{&#x27;active&#x27;: prTabState?.active == &#x27;diff&#x27;}" @click="prTabState.active = &#x27;diff&#x27;">
class="paper-tab"
ng-if="options.diff"
ng-class="{'active': prTabState.active == 'diff'}"
ng-click="prTabState.active = 'diff'"
type="button"
role="tab"
>
<i class="fas fa-code"></i> Diff <i class="fas fa-code"></i> Diff
</button> </button>
<button <button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{&#x27;active&#x27;: prTabState?.active == &#x27;comments&#x27;}" @click="prTabState.active = &#x27;comments&#x27;">
class="paper-tab"
ng-if="options.comments"
ng-class="{'active': prTabState.active == 'comments'}"
ng-click="prTabState.active = 'comments'"
type="button"
role="tab"
>
<i class="far fa-comment-dots"></i> <i class="far fa-comment-dots"></i>
<ng-pluralize count="details.pullRequest.comments.length" when="{'0': 'No comments', 'one': '1 comment', 'other': '{} comments'}"></ng-pluralize> <span v-text="fmt.plural(details?.pullRequest?.comments?.length, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></span>
</button> </button>
</nav> </nav>
<div class="paper-tab-content"> <div class="paper-tab-content">
<div ng-if="options.diff && prTabState.active == 'diff'"> <div v-if="options?.diff &amp;&amp; prTabState?.active == &#x27;diff&#x27;">
<div class="pr-diff" ng-bind-html="anonymizePrContent(details.pullRequest.diff) | diff"></div> <div class="pr-diff" v-html="sanitize(fmt?.diff(anonymizePrContent(details?.pullRequest?.diff)))"></div>
</div> </div>
<div ng-if="options.comments && prTabState.active == 'comments'"> <div v-if="options?.comments &amp;&amp; prTabState?.active == &#x27;comments&#x27;">
<ul class="pr-comments"> <ul class="pr-comments">
<li class="pr-comment" ng-repeat="comment in details.pullRequest.comments"> <li class="pr-comment" v-for="(comment, index) in details?.pullRequest?.comments">
<div class="pr-comment-head"> <div class="pr-comment-head">
<span class="pr-comment-author" ng-if="options.username"> <span class="pr-comment-author" v-if="options?.username">
<i class="far fa-user"></i> @<span ng-bind="anonymizePrContent(comment.author)"></span> <i class="far fa-user"></i> @<span v-text="anonymizePrContent(comment?.author)"></span>
</span> </span>
<span class="pr-comment-date" ng-if="options.date" ng-bind="comment.updatedDate | date"></span> <span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
</div> </div>
<div class="pr-comment-body" ng-if="options.body"> <div class="pr-comment-body" v-if="options?.body">
<markdown content="anonymizePrContent(comment.body)" options="options" terms="terms"></markdown> <markdown :content="anonymizePrContent(comment?.body)" :options="options" :terms="terms"></markdown>
</div> </div>
</li> </li>
</ul> </ul>
+94 -380
View File
@@ -1,18 +1,8 @@
<div class="container-fluid h-100 anonymize-page"> <div class="container-fluid h-100 anonymize-page">
<div class="row h-100 flex-column flex-md-row"> <div class="row h-100 flex-column flex-md-row">
<div <div class="col-md sidePanel shadow overflow-auto anonymize-form-col">
class="col-md sidePanel shadow overflow-auto anonymize-form-col" <div class="p-0 py-2 m-auto" :class="{&#x27;card&#x27;: !pullRequestUrl,&#x27;container&#x27;: pullRequestUrl}">
> <form class="form needs-validation" name="anonymizeForm" novalidate v-form="viewState" :class="{&#x27;card-body&#x27;: !pullRequestUrl}">
<div
class="p-0 py-2 m-auto"
ng-class="{'card': !pullRequestUrl,'container': pullRequestUrl}"
>
<form
class="form needs-validation"
ng-class="{'card-body': !pullRequestUrl}"
name="anonymizeForm"
novalidate
>
<div class="paper-crumbs">Anonymize &nbsp;/&nbsp; <span class="here">Pull request</span></div> <div class="paper-crumbs">Anonymize &nbsp;/&nbsp; <span class="here">Pull request</span></div>
<h1 class="paper-page-title">Anonymize a <em>pull request</em></h1> <h1 class="paper-page-title">Anonymize a <em>pull request</em></h1>
<p class="paper-page-lede"> <p class="paper-page-lede">
@@ -24,56 +14,27 @@
</div> </div>
<!-- pullRequestUrl --> <!-- pullRequestUrl -->
<div class="form-group"> <div class="form-group">
<label for="pullRequestUrl" <label for="pullRequestUrl">URL of your pull request</label>
>URL of your pull request</label <input type="text" class="form-control" name="pullRequestUrl" id="pullRequestUrl" placeholder="https://github.com/owner/repo/pull/123" v-field="{ state: viewState, set: value =&gt; { pullRequestUrl = value }, value: pullRequestUrl, form: &quot;anonymizeForm&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { pullRequestSelected() } }" :class="{&#x27;is-invalid&#x27;: anonymize?.pullRequestUrl?.invalid}">
> <div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.access">
<input {{ pullRequestUrl }} is not accessible. Some organizations are
type="text"
class="form-control"
name="pullRequestUrl"
id="pullRequestUrl"
ng-class="{'is-invalid': anonymize.pullRequestUrl.$invalid}"
ng-model="pullRequestUrl"
placeholder="https://github.com/owner/repo/pull/123"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
ng-change="pullRequestSelected()"
/>
<div
class="invalid-feedback"
ng-show="anonymize.pullRequestUrl.$error.access"
>
{{pullRequestUrl}} is not accessible. Some organizations are
restricting the access to the repositories. restricting the access to the repositories.
</div> </div>
<div <div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.missing">
class="invalid-feedback" {{ pullRequestUrl }} does not exist or is not accessible
ng-show="anonymize.pullRequestUrl.$error.missing"
>
{{pullRequestUrl}} does not exist or is not accessible
</div> </div>
<div <div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.used">
class="invalid-feedback" {{ pullRequestUrl }} is already anonymized
ng-show="anonymize.pullRequestUrl.$error.used"
>
{{pullRequestUrl}} is already anonymized
</div> </div>
</div> </div>
<div ng-show="pullRequestUrl"> <div v-show="pullRequestUrl">
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value =&gt; { options.update = value }, value: options?.update, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input"
type="checkbox"
id="update"
name="update"
ng-model="options.update"
/>
<label class="form-check-label" for="update">Auto update</label> <label class="form-check-label" for="update">Auto update</label>
<small id="updateHelp" class="form-text text-muted" <small id="updateHelp" class="form-text text-muted">Automatically update the anonymized pull request with the
>Automatically update the anonymized pull request with the
latest updates. The pull request is updated once per day latest updates. The pull request is updated once per day
maximum.</small maximum.</small>
>
</div> </div>
</div> </div>
@@ -82,31 +43,15 @@
</div> </div>
<!-- Conference --> <!-- Conference -->
<div class="form-group"> <div class="form-group">
<label for="conference" <label for="conference">Conference ID <span class="text-muted">(Optional)</span></label>
>Conference ID <span class="text-muted">(Optional)</span></label <input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value =&gt; { conference = value }, value: conference, form: &quot;anonymizeForm&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.conference?.invalid}">
> <small class="form-text text-muted" v-show="conference_data"><a target="_target" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a>
<input will expire on {{ fmt?.date(conference_data?.endDate) }}.</small>
class="form-control"
id="conference"
name="conference"
ng-model="conference"
ng-model-options="{ debounce: { default: 800, blur: 0 }, updateOn: 'default blur' }"
ng-class="{'is-invalid': anonymize.conference.$invalid}"
/>
<small class="form-text text-muted" ng-show="conference_data"
><a ng-href="{{conference_data.url}}" target="_target"
>{{conference_data.name}}</a
>
will expire on {{conference_data.endDate | date}}.</small
>
<div <div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">
class="invalid-feedback"
ng-show="anonymize.conference.$error.activated"
>
The conference is not activated. The conference is not activated.
</div> </div>
<small class="form-text text-muted" ng-show="!conference_data"> <small class="form-text text-muted" v-show="!conference_data">
Use the Conference ID that your conference provided you. This Use the Conference ID that your conference provided you. This
will update automatically the anonymization options based on the will update automatically the anonymization options based on the
conference preferences. conference preferences.
@@ -119,247 +64,101 @@
<!-- Pull Request ID --> <!-- Pull Request ID -->
<div class="form-group"> <div class="form-group">
<label for="pullRequestId">Anonymized pull request id</label> <label for="pullRequestId">Anonymized pull request id</label>
<input <input type="text" class="form-control" name="pullRequestId" id="pullRequestId" v-field="{ state: viewState, set: value =&gt; { pullRequestId = value }, value: pullRequestId, form: &quot;anonymizeForm&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.pullRequestId?.invalid}">
type="text" <small id="idHelp" class="form-text text-muted">Id used in the url:
class="form-control" https://anonymous.4open.science/r/{{ pullRequestId }}</small>
name="pullRequestId" <div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">
id="pullRequestId"
ng-class="{'is-invalid': anonymize.pullRequestId.$invalid}"
ng-model="pullRequestId"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
/>
<small id="idHelp" class="form-text text-muted"
>Id used in the url:
https://anonymous.4open.science/r/{{pullRequestId}}</small
>
<div
class="invalid-feedback"
ng-show="anonymize.pullRequestId.$error.format"
>
Repository id can only contain letters and numbers Repository id can only contain letters and numbers
</div> </div>
<div <div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">
class="invalid-feedback" {{ pullRequestId }} is already used
ng-show="anonymize.pullRequestId.$error.used"
>
{{pullRequestId}} is already used
</div> </div>
</div> </div>
<!-- Terms --> <!-- Terms -->
<div class="form-group"> <div class="form-group">
<label for="terms">Terms to anonymize</label> <label for="terms">Terms to anonymize</label>
<textarea <textarea class="form-control" id="terms" name="terms" rows="3" v-field="{ state: viewState, set: value =&gt; { terms = value }, value: terms, form: &quot;anonymizeForm&quot;, options: { debounce: 250 } }" :class="{&#x27;is-invalid&#x27;: anonymize?.terms?.invalid}"></textarea>
class="form-control" <small id="termsHelp" class="form-text text-muted">One term per line. Each term will be replaced by XXX.</small>
id="terms" <div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">
name="terms"
rows="3"
ng-model="terms"
ng-model-options="{ debounce: 250 }"
ng-class="{'is-invalid': anonymize.terms.$invalid}"
></textarea>
<small id="termsHelp" class="form-text text-muted"
>One term per line. Each term will be replaced by XXX.</small
>
<div
class="invalid-feedback"
ng-show="anonymize.terms.$error.format"
>
Terms are in an invalid format Terms are in an invalid format
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="expiration">Expiration strategy</label> <label for="expiration">Expiration strategy</label>
<select <select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-control"
id="expiration"
name="expiration"
ng-model="options.expirationMode"
>
<option value="never" selected>Never expire</option> <option value="never" selected>Never expire</option>
<option value="redirect"> <option value="redirect">
Redirect to GitHub when expired Redirect to GitHub when expired
</option> </option>
<option value="remove">Remove when expired</option> <option value="remove">Remove when expired</option>
</select> </select>
<small class="form-text text-muted" <small class="form-text text-muted">Define the expiration strategy for the anonymized
>Define the expiration strategy for the anonymized repository.</small>
repository.</small
>
</div> </div>
<div <div class="form-group" id="expiration-date-form" v-show="!(options?.expirationMode==&#x27;never&#x27;)">
class="form-group" <label for="expirationDate">Expiration date of the anonymized repository</label>
id="expiration-date-form" <input class="form-control" type="date" name="expirationDate" id="expirationDate" v-field="{ state: viewState, set: value =&gt; { options.expirationDate = value }, value: options?.expirationDate, form: &quot;anonymizeForm&quot;, options: {} }">
ng-hide="options.expirationMode=='never'" <small class="form-text text-muted" v-show="options?.expirationMode==&#x27;remove&#x27;">After {{ fmt?.date(options?.expirationDate) }}, the repository will be
>
<label for="expirationDate"
>Expiration date of the anonymized repository</label
>
<input
class="form-control"
type="date"
name="expirationDate"
id="expirationDate"
ng-model="options.expirationDate"
/>
<small
class="form-text text-muted"
ng-show="options.expirationMode=='remove'"
>After {{options.expirationDate | date}}, the repository will be
removed and the visitor will not be able to see the content of removed and the visitor will not be able to see the content of
the repository.</small the repository.</small>
> <small class="form-text text-muted" v-show="options?.expirationMode==&#x27;redirect&#x27;">After {{ fmt?.date(options?.expirationDate) }}, the visitors of the
<small
class="form-text text-muted"
ng-show="options.expirationMode=='redirect'"
>After {{options.expirationDate | date}}, the visitors of the
anonymized repository will be redirected to anonymized repository will be redirected to
{{pullRequestUrl}}.</small {{ pullRequestUrl }}.</small>
>
</div> </div>
<div class="accordion mb-3" id="options"> <div class="accordion mb-3" id="options">
<div class="card"> <div class="card">
<div class="card-header" id="headingOne"> <div class="card-header" id="headingOne">
<h2 class="mb-0"> <h2 class="mb-0">
<button <button class="btn btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
class="btn btn-block text-left"
type="button"
data-toggle="collapse"
data-target="#collapseOne"
aria-expanded="true"
aria-controls="collapseOne"
>
<i class="fas fa-cog mr-1"></i> Advanced options <i class="fas fa-cog mr-1"></i> Advanced options
</button> </button>
</h2> </h2>
</div> </div>
<div <div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#options">
id="collapseOne"
class="collapse show"
aria-labelledby="headingOne"
data-parent="#options"
>
<div class="card-body"> <div class="card-body">
<div class="form-group mb-0"> <div class="form-group mb-0">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value =&gt; { options.link = value }, value: options?.link, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="link">Keep links</label>
type="checkbox" <small class="form-text text-muted">Keep or remove all the links.</small>
id="link"
name="link"
ng-model="options.link"
/>
<label class="form-check-label" for="link"
>Keep links</label
>
<small class="form-text text-muted"
>Keep or remove all the links.</small
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value =&gt; { options.image = value }, value: options?.image, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="image">Display images</label>
type="checkbox" <small class="form-text text-muted">Images are not anonymized</small>
id="image"
name="image"
ng-model="options.image"
/>
<label class="form-check-label" for="image"
>Display images</label
>
<small class="form-text text-muted"
>Images are not anonymized</small
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="date" name="date" v-field="{ state: viewState, set: value =&gt; { options.date = value }, value: options?.date, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="date">Display dates</label>
type="checkbox" <small class="form-text text-muted">Display the date of the Pull Request and the date of
id="date" the comments.</small>
name="date"
ng-model="options.date"
/>
<label class="form-check-label" for="date"
>Display dates</label
>
<small class="form-text text-muted"
>Display the date of the Pull Request and the date of
the comments.</small
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="username" name="username" v-field="{ state: viewState, set: value =&gt; { options.username = value }, value: options?.username, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="username">Display username</label>
type="checkbox"
id="username"
name="username"
ng-model="options.username"
/>
<label class="form-check-label" for="username"
>Display username</label
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="comments" name="comments" v-field="{ state: viewState, set: value =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="comments">Display comments</label>
type="checkbox"
id="comments"
name="comments"
ng-model="options.comments"
/>
<label class="form-check-label" for="comments"
>Display comments</label
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="diff" name="diff" v-field="{ state: viewState, set: value =&gt; { options.diff = value }, value: options?.diff, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="diff">Display diff</label>
type="checkbox"
id="diff"
name="diff"
ng-model="options.diff"
/>
<label class="form-check-label" for="diff"
>Display diff</label
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="origin" name="origin" v-field="{ state: viewState, set: value =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="origin">Display the project name</label>
type="checkbox"
id="origin"
name="origin"
ng-model="options.origin"
/>
<label class="form-check-label" for="origin"
>Display the project name</label
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="title" name="title" v-field="{ state: viewState, set: value =&gt; { options.title = value }, value: options?.title, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="title">Display the PR title</label>
type="checkbox"
id="title"
name="title"
ng-model="options.title"
/>
<label class="form-check-label" for="title"
>Display the PR title</label
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="body" name="body" v-field="{ state: viewState, set: value =&gt; { options.body = value }, value: options?.body, form: &quot;anonymizeForm&quot;, options: {} }">
class="form-check-input" <label class="form-check-label" for="body">Display the PR body and comment bodies</label>
type="checkbox"
id="body"
name="body"
ng-model="options.body"
/>
<label class="form-check-label" for="body"
>Display the PR body and comment bodies</label
>
</div> </div>
</div> </div>
</div> </div>
@@ -367,149 +166,64 @@
</div> </div>
</div> </div>
</div> </div>
<div <div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
class="alert alert-danger" <div class="anonymize-submit-bar" v-show="pullRequestUrl">
role="alert" <button id="submit" type="submit" class="btn btn-ink btn-block" v-if="!isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizePullRequest($event) })">
ng-if="error"
ng-bind="error"
></div>
<div class="anonymize-submit-bar" ng-show="pullRequestUrl">
<button
id="submit"
type="submit"
class="btn btn-ink btn-block"
ng-click="anonymizePullRequest($event)"
ng-if="!isUpdate"
>
<i class="fas fa-user-secret mr-1"></i> Anonymize <i class="fas fa-user-secret mr-1"></i> Anonymize
</button> </button>
<button <button id="submit" type="submit" class="btn btn-ink btn-block" v-if="isUpdate" @click.prevent="submitForm($event, () =&gt; { updatePullRequest($event) })">
id="submit"
type="submit"
class="btn btn-ink btn-block"
ng-click="updatePullRequest($event)"
ng-if="isUpdate"
>
<i class="fas fa-save mr-1"></i> Update <i class="fas fa-save mr-1"></i> Update
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div class="col-md-8 p-2 overflow-auto anonymize-preview-col" ng-if="details"> <div class="col-md-8 p-2 overflow-auto anonymize-preview-col" v-if="details">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap"> <div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1"> <h2 class="pr-title mb-1">
<span ng-if="options.title" <span v-if="options?.title">{{ anonymize(details?.pullRequest?.title) }}</span>
>{{anonymize(details.pullRequest.title)}}</span <span class="badge" :class="{&#x27;badge-success&#x27;:details?.pullRequest?.merged, &#x27;badge-warning&#x27;:details?.pullRequest?.state==&#x27;open&#x27;, &#x27;badge-danger&#x27;:details?.pullRequest?.state==&#x27;closed&#x27; &amp;&amp;!details?.pullRequest?.merged}">
> {{ fmt?.title(details?.pullRequest?.merged?"merged":details?.pullRequest?.state) }}
<span
class="badge"
ng-class="{'badge-success':details.pullRequest.merged, 'badge-warning':details.pullRequest.state=='open', 'badge-danger':details.pullRequest.state=='closed' &&!details.pullRequest.merged}"
>
{{details.pullRequest.merged?"merged":details.pullRequest.state |
title}}
</span> </span>
</h2> </h2>
<small <small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
ng-bind="details.pullRequest.updatedDate | date"
ng-if="options.date"
></small>
</div> </div>
<small ng-if="options.origin" <small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
>Pull Request on {{details.pullRequest.baseRepositoryFullName}}</small <div class="pr-body shadow-sm p-3 mb-4 rounded" style="background: var(--sidebar-bg-color)" v-if="options?.body">
> <markdown :content="anonymize(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
<div
class="pr-body shadow-sm p-3 mb-4 rounded"
style="background: var(--sidebar-bg-color)"
ng-if="options.body"
>
<markdown
content="anonymize(details.pullRequest.body)"
options="options"
terms="terms"
></markdown>
</div> </div>
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation" ng-if="options.diff"> <li class="nav-item" role="presentation" v-if="options?.diff">
<button <button class="nav-link active" id="pills-diff-tab" data-toggle="pill" data-target="#pills-diff" type="button" role="tab" aria-controls="pills-diff" aria-selected="true">
class="nav-link active"
id="pills-diff-tab"
data-toggle="pill"
data-target="#pills-diff"
type="button"
role="tab"
aria-controls="pills-diff"
aria-selected="true"
>
Diff Diff
</button> </button>
</li> </li>
<li class="nav-item" role="presentation" ng-if="options.comments"> <li class="nav-item" role="presentation" v-if="options?.comments">
<button <button class="nav-link" id="pills-comments-tab" data-toggle="pill" data-target="#pills-comments" type="button" role="tab" aria-controls="pills-comments" aria-selected="false" :class="{&#x27;active&#x27;:!options?.diff}">
class="nav-link" <span v-text="fmt.plural(details?.pullRequest?.comments?.length, {&#x27;0&#x27;: &#x27;No comment&#x27;,
ng-class="{'active':!options.diff}" &#x27;one&#x27;: &#x27;One Comment&#x27;,
id="pills-comments-tab" &#x27;other&#x27;: &#x27;{} Comments&#x27;})">
data-toggle="pill" </span>
data-target="#pills-comments"
type="button"
role="tab"
aria-controls="pills-comments"
aria-selected="false"
>
<ng-pluralize
count="details.pullRequest.comments.length"
when="{'0': 'No comment',
'one': 'One Comment',
'other': '{} Comments'}"
>
</ng-pluralize>
</button> </button>
</li> </li>
</ul> </ul>
<div class="tab-content" id="pills-tabContent"> <div class="tab-content" id="pills-tabContent">
<div <div class="tab-pane show active" id="pills-diff" role="tabpanel" aria-labelledby="pills-diff-tab">
class="tab-pane show active" <div class="pr-diff shadow-sm p-3 mb-4 rounded" style="background: var(--sidebar-bg-color)" v-if="options?.diff">
id="pills-diff" <pre style="overflow-x: auto"><code v-html="sanitize(fmt?.diff(anonymize(details?.pullRequest?.diff)))"></code></pre>
role="tabpanel"
aria-labelledby="pills-diff-tab"
>
<div
class="pr-diff shadow-sm p-3 mb-4 rounded"
style="background: var(--sidebar-bg-color)"
ng-if="options.diff"
>
<pre style="overflow-x: auto"><code ng-bind-html="anonymize(details.pullRequest.diff) | diff"></code></pre>
</div> </div>
</div> </div>
<div <div class="tab-pane" id="pills-comments" role="tabpanel" aria-labelledby="pills-comments-tab" :class="{&#x27;show active&#x27;:!options?.diff}">
class="tab-pane" <ul class="pr-comments list-group" v-if="options?.comments">
ng-class="{'show active':!options.diff}" <li class="pr-comment list-group-item" v-for="(comment, index) in details?.pullRequest?.comments">
id="pills-comments"
role="tabpanel"
aria-labelledby="pills-comments-tab"
>
<ul class="pr-comments list-group" ng-if="options.comments">
<li
class="pr-comment list-group-item"
ng-repeat="comment in details.pullRequest.comments"
>
<div class="d-flex w-100 justify-content-between flex-wrap"> <div class="d-flex w-100 justify-content-between flex-wrap">
<h5 class="mb-1" ng-if="options.username"> <h5 class="mb-1" v-if="options?.username">
@{{anonymize(comment.author)}} @{{ anonymize(comment?.author) }}
</h5> </h5>
<small <small v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></small>
ng-bind="comment.updatedDate | date"
ng-if="options.date"
></small>
</div> </div>
<p class="mb-1"> <p class="mb-1">
<markdown <markdown class="pr-comment-body" v-if="options?.body" :content="anonymize(comment?.body)" :options="options" :terms="terms"></markdown>
class="pr-comment-body"
ng-if="options.body"
content="anonymize(comment.body)"
options="options"
terms="terms"
></markdown>
</p> </p>
</li> </li>
</ul> </ul>
+6 -28
View File
@@ -7,43 +7,21 @@
</p> </p>
<div class="paper-settings-main claim-form"> <div class="paper-settings-main claim-form">
<p class="paper-section-copy">Use this when an anonymization was created by a co-author or from another account. You must have access to the GitHub repository it was made from.</p> <p class="paper-section-copy">Use this when an anonymization was created by a co-author or from another account. You must have access to the GitHub repository it was made from.</p>
<form class="form needs-validation" name="claimForm" novalidate ng-submit="claim()"> <form class="form needs-validation" name="claimForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () =&gt; { claim() })">
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="repoUrl">GitHub repository URL</label> <label class="paper-field-label" for="repoUrl">GitHub repository URL</label>
<input <input type="text" class="form-control" name="repoUrl" id="repoUrl" required v-field="{ state: viewState, set: value =&gt; { repoUrl = value }, value: repoUrl, form: &quot;claimForm&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: claimForm?.repoUrl?.invalid &amp;&amp; (claimForm?.repoUrl?.touched || claimForm?.submitted)}">
type="text" <div class="invalid-feedback" v-show="claimForm?.repoUrl?.errors?.not_found">
class="form-control"
name="repoUrl"
id="repoUrl"
required
ng-class="{'is-invalid': claimForm.repoUrl.$invalid && (claimForm.repoUrl.$touched || claimForm.$submitted)}"
ng-model="repoUrl"
/>
<div class="invalid-feedback" ng-show="claimForm.repoUrl.$error.not_found">
No anonymization matches this repository and ID. Check both values and that you can access the repository on GitHub. No anonymization matches this repository and ID. Check both values and that you can access the repository on GitHub.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="repoId">Anonymized repository ID</label> <label class="paper-field-label" for="repoId">Anonymized repository ID</label>
<input <input type="text" class="form-control" name="repoId" id="repoId" required v-field="{ state: viewState, set: value =&gt; { repoId = value }, value: repoId, form: &quot;claimForm&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: claimForm?.repoId?.invalid &amp;&amp; (claimForm?.repoId?.touched || claimForm?.submitted)}">
type="text" <small id="idHelp" class="form-text text-muted">The ID is the last part of the anonymized URL: <code>anonymous.4open.science/r/&lt;ID&gt;</code>.</small>
class="form-control"
name="repoId"
id="repoId"
required
ng-model="repoId"
ng-class="{'is-invalid': claimForm.repoId.$invalid && (claimForm.repoId.$touched || claimForm.$submitted)}"
/>
<small id="idHelp" class="form-text text-muted"
>The ID is the last part of the anonymized URL: <code>anonymous.4open.science/r/&lt;ID&gt;</code>.</small
>
</div> </div>
<button <button id="submit" type="submit" class="btn btn-ink">
id="submit"
type="submit"
class="btn btn-ink"
>
Claim anonymization Claim anonymization
</button> </button>
</form> </form>
+53 -87
View File
@@ -1,99 +1,77 @@
<div class="container page paper-page"> <div class="container page paper-page">
<div> <div>
<div class="paper-crumbs"> <div class="paper-crumbs">
<a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <span class="here">{{conference.conferenceID}}</span> <a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <span class="here">{{ conference?.conferenceID }}</span>
</div> </div>
<div class="d-flex align-items-end flex-wrap" style="gap: 12px; justify-content: space-between;"> <div class="d-flex align-items-end flex-wrap" style="gap: 12px; justify-content: space-between;">
<div> <div>
<h1 class="paper-page-title" ng-bind="conference.name"></h1> <h1 class="paper-page-title" v-text="conference?.name"></h1>
<p class="paper-page-lede" ng-if="conference.url"> <p class="paper-page-lede" v-if="conference?.url">
<a ng-href="{{conference.url}}" target="_blank" rel="noopener" ng-bind="conference.url"></a> <a target="_blank" rel="noopener" v-text="conference?.url" :href="safeUrl((conference?.url))"></a>
</p> </p>
</div> </div>
<div class="d-flex align-items-center flex-wrap" style="gap: 10px;"> <div class="d-flex align-items-center flex-wrap" style="gap: 10px;">
<span class="status-pill" ng-class="{'status-pill-ready': conference.status == 'ready', 'status-pill-removed': conference.status == 'removed' || conference.status == 'expired'}"> <span class="status-pill" :class="{&#x27;status-pill-ready&#x27;: conference?.status == &#x27;ready&#x27;, &#x27;status-pill-removed&#x27;: conference?.status == &#x27;removed&#x27; || conference?.status == &#x27;expired&#x27;}">
<span class="status-dot" ng-class="'status-' + conference.status" aria-hidden="true"></span> <span class="status-dot" aria-hidden="true" :class="&#x27;status-&#x27; + conference?.status"></span>
<span ng-bind="conference.status | statusLabel"></span> <span v-text="fmt?.statusLabel(conference?.status)"></span>
</span> </span>
<a class="btn btn-outline-ink" ng-href="/conference/{{conference.conferenceID}}/edit"><i class="far fa-edit mr-1" aria-hidden="true"></i> Edit conference</a> <a class="btn btn-outline-ink" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/edit&quot;)"><i class="far fa-edit mr-1" aria-hidden="true"></i> Edit conference</a>
</div> </div>
</div> </div>
<div class="paper-meta-rule"> <div class="paper-meta-rule">
<span>ID <b class="commit-hash" ng-bind="conference.conferenceID"></b></span> <span>ID <b class="commit-hash" v-text="conference?.conferenceID"></b></span>
<span>Review window <b>{{conference.startDate | date:'mediumDate'}} &ndash; {{conference.endDate | date:'mediumDate'}}</b></span> <span>Review window <b>{{ fmt?.date(conference?.startDate, 'mediumDate') }} &ndash; {{ fmt?.date(conference?.endDate, 'mediumDate') }}</b></span>
<span>Repositories <b>{{conference.repositories.length | number}}</b></span> <span>Repositories <b>{{ fmt?.number(conference?.repositories?.length) }}</b></span>
<span>Plan <b ng-bind="conference.plan.name || conference.plan.planID || 'Free'"></b></span> <span>Plan <b v-text="conference?.plan?.name || conference?.plan?.planID || &#x27;Free&#x27;"></b></span>
<span ng-if="conference.price">Cost so far <b>{{conference.price | number:2}} &euro;</b></span> <span v-if="conference?.price">Cost so far <b>{{ fmt?.number(conference?.price, 2) }} &euro;</b></span>
</div> </div>
<div class="paper-section-eyebrow">Repositories</div> <div class="paper-section-eyebrow">Repositories</div>
<form class="w-100 dashboard-filter-row" aria-label="Filter repositories" accept-charset="UTF-8" ng-submit="$event.preventDefault()"> <form class="w-100 dashboard-filter-row" aria-label="Filter repositories" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () =&gt; { $event.preventDefault() })">
<div class="search-wrap"> <div class="search-wrap">
<input <input type="search" id="search" class="form-control" aria-label="Search repositories" placeholder="Search by ID or source repository…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { search = value }, value: search, form: null, options: {} }">
type="search"
id="search"
class="form-control"
aria-label="Search repositories"
placeholder="Search by ID or source repository…"
autocomplete="off"
ng-model="search"
/>
</div> </div>
<div class="dashboard-filter-controls"> <div class="dashboard-filter-controls">
<div class="dropdown"> <div class="dropdown">
<button <button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn dropdown-toggle"
type="button"
id="dropdownSort"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="filter-btn-label">Sort</span> <span class="filter-btn-label">Sort</span>
<span class="filter-btn-value">{{ ({'-anonymizeDate': 'Anonymize date', repoId: 'ID', '-status': 'Status', '-pageView': 'Views'})[orderBy] || 'Custom' }}</span> <span class="filter-btn-value">{{ ({'-anonymizeDate': 'Anonymize date', repoId: 'ID', '-status': 'Status', '-pageView': 'Views'})[orderBy] || 'Custom' }}</span>
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownSort"> <div class="dropdown-menu" aria-labelledby="dropdownSort">
<h6 class="dropdown-header">Order by</h6> <h6 class="dropdown-header">Order by</h6>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="anonymizeDate" value="-anonymizeDate" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="anonymizeDate" value="-anonymizeDate" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="anonymizeDate">Anonymize date</label> <label class="form-check-label" for="anonymizeDate">Anonymize date</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortID" value="repoId" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortID" value="repoId" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortID">ID</label> <label class="form-check-label" for="sortID">ID</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortStatus">Status</label> <label class="form-check-label" for="sortStatus">Status</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortViews" value="-pageView" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortViews" value="-pageView" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortViews">Views</label> <label class="form-check-label" for="sortViews">Views</label>
</div> </div>
</div> </div>
</div> </div>
<div class="dropdown" ng-init="statusLabels = {ready: 'Ready', expired: 'Expired', removed: 'Removed'}"> <div class="dropdown">
<button <button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn dropdown-toggle"
type="button"
id="dropdownStatus"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="filter-btn-label">Status</span> <span class="filter-btn-label">Status</span>
<span class="filter-btn-value" ng-if="filters.status.ready && filters.status.expired && filters.status.removed">All</span> <span class="filter-btn-value" v-if="filters?.status?.ready &amp;&amp; filters?.status?.expired &amp;&amp; filters?.status?.removed">All</span>
<span class="filter-btn-value" ng-if="!(filters.status.ready && filters.status.expired && filters.status.removed)"> <span class="filter-btn-value" v-if="!(filters?.status?.ready &amp;&amp; filters?.status?.expired &amp;&amp; filters?.status?.removed)">
{{ (filters.status.ready ? 0 : 1) + (filters.status.expired ? 0 : 1) + (filters.status.removed ? 0 : 1) }} hidden {{ (filters?.status?.ready ? 0 : 1) + (filters?.status?.expired ? 0 : 1) + (filters?.status?.removed ? 0 : 1) }} hidden
</span> </span>
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownStatus"> <div class="dropdown-menu" aria-labelledby="dropdownStatus">
<h6 class="dropdown-header">Show statuses</h6> <h6 class="dropdown-header">Show statuses</h6>
<div class="form-check dropdown-item" ng-repeat="(key, label) in statusLabels"> <div class="form-check dropdown-item" v-for="(label, key, index) in statusLabels">
<input class="form-check-input" type="checkbox" id="status-{{key}}" ng-model="filters.status[key]" /> <input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value =&gt; { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id="&quot;status-&quot; + (key)">
<label class="form-check-label" for="status-{{key}}">{{label}}</label> <label class="form-check-label" :for="&quot;status-&quot; + (key)">{{ label }}</label>
</div> </div>
</div> </div>
</div> </div>
@@ -102,13 +80,13 @@
<div class="dashboard-meta"> <div class="dashboard-meta">
<span class="dashboard-count"> <span class="dashboard-count">
<span ng-if="filteredRepositories.length === conference.repositories.length">{{conference.repositories.length | number}} repositor<span ng-if="conference.repositories.length === 1">y</span><span ng-if="conference.repositories.length !== 1">ies</span></span> <span v-if="filteredRepositories?.length === conference?.repositories?.length">{{ fmt?.number(conference?.repositories?.length) }} repositor<span v-if="conference?.repositories?.length === 1">y</span><span v-if="conference?.repositories?.length !== 1">ies</span></span>
<span ng-if="filteredRepositories.length !== conference.repositories.length">{{filteredRepositories.length | number}} of {{conference.repositories.length | number}} shown</span> <span v-if="filteredRepositories?.length !== conference?.repositories?.length">{{ fmt?.number(filteredRepositories?.length) }} of {{ fmt?.number(conference?.repositories?.length) }} shown</span>
</span>
<span class="filter-chip" ng-repeat="(f, v) in filters.status" ng-if="v === false">
Hiding {{statusLabels[f]}}
<button type="button" class="filter-chip-close" aria-label="Show {{statusLabels[f]}} again" ng-click="filters.status[f] = true;">&times;</button>
</span> </span>
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
Hiding {{ statusLabels[f] }}
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label="&quot;Show &quot; + (statusLabels[f]) + &quot; again&quot;">&times;</button>
</span></template>
</div> </div>
<div class="paper-table paper-table-repos w-100" role="table" aria-label="Repositories"> <div class="paper-table paper-table-repos w-100" role="table" aria-label="Repositories">
@@ -119,66 +97,54 @@
<div role="columnheader">Expires</div> <div role="columnheader">Expires</div>
<div role="columnheader"><span class="sr-only">Actions</span></div> <div role="columnheader"><span class="sr-only">Actions</span></div>
</div> </div>
<div <div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :key="repo?.repoId" :class="{&#x27;repo-inactive&#x27;: repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;, &#x27;repo-error&#x27;: repo?.status == &#x27;error&#x27;}">
class="paper-table-row"
role="row"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'repo-error': repo.status == 'error'}"
ng-repeat="repo in conference.repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories track by repo.repoId"
>
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<span class="type-badge type-repo">Repo</span> <span class="type-badge type-repo">Repo</span>
<div class="anon-text"> <div class="anon-text">
<a class="repo-name" ng-href="/r/{{repo.repoId}}/" ng-bind="repo.repoId"></a> <a class="repo-name" v-text="repo?.repoId" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)"></a>
<div class="anon-sub"> <div class="anon-sub">
<span class="anon-source"> <span class="anon-source">
<a href="https://github.com/{{repo.source.fullName}}/" target="_blank" rel="noopener" ng-bind="repo.source.fullName"></a><span ng-if="repo.options.update && repo.source.branch"> &middot; <a href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.branch}}" target="_blank" rel="noopener" ng-bind="repo.source.branch"></a></span><span ng-if="repo.source.commit"> &middot; <a class="commit-hash" href="https://github.com/{{repo.source.fullName}}/tree/{{repo.source.commit}}" target="_blank" rel="noopener">@{{repo.source.commit.substring(0, 8)}}</a></span> <a target="_blank" rel="noopener" v-text="repo?.source?.fullName" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/&quot;)"></a><span v-if="repo?.options?.update &amp;&amp; repo?.source?.branch"> &middot; <a target="_blank" rel="noopener" v-text="repo?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.branch))"></a></span><span v-if="repo?.source?.commit"> &middot; <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl(&quot;https://github.com/&quot; + (repo?.source?.fullName) + &quot;/tree/&quot; + (repo?.source?.commit))">@{{ repo?.source?.commit?.substring(0, 8) }}</a></span>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<div class="status-line"> <div class="status-line">
<span class="status-dot" ng-class="'status-' + (repo.status == 'error' ? 'error' : repo.status == 'ready' ? 'ready' : (repo.status == 'expired' || repo.status == 'removed') ? repo.status : 'progress')" aria-hidden="true"></span> <span class="status-dot" aria-hidden="true" :class="&#x27;status-&#x27; + (repo?.status == &#x27;error&#x27; ? &#x27;error&#x27; : repo?.status == &#x27;ready&#x27; ? &#x27;ready&#x27; : (repo?.status == &#x27;expired&#x27; || repo?.status == &#x27;removed&#x27;) ? repo?.status : &#x27;progress&#x27;)"></span>
<span class="status-word" ng-bind="repo.status | statusLabel"></span> <span class="status-word" v-text="fmt?.statusLabel(repo?.status)"></span>
</div> </div>
<div class="status-sub" ng-if="repo.anonymizeDate">anonymized {{repo.anonymizeDate | humanTime}}</div> <div class="status-sub" v-if="repo?.anonymizeDate">anonymized {{ fmt?.humanTime(repo?.anonymizeDate) }}</div>
</div> </div>
<div class="cell-views num" role="cell" ng-bind="::repo.pageView | number"></div> <div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView)"></div>
<div class="cell-expires" role="cell"> <div class="cell-expires" role="cell">
<span class="expires-never" ng-if="repo.status == 'ready' && (repo.options.expirationMode === 'never' || !repo.options.expirationDate)">Never</span> <span class="expires-never" v-if="repo?.status == &#x27;ready&#x27; &amp;&amp; (repo?.options?.expirationMode === &#x27;never&#x27; || !repo?.options?.expirationDate)">Never</span>
<span ng-if="repo.status == 'ready' && repo.options.expirationMode !== 'never' && repo.options.expirationDate" ng-bind="repo.options.expirationDate | humanTime"></span> <span v-if="repo?.status == &#x27;ready&#x27; &amp;&amp; repo?.options?.expirationMode !== &#x27;never&#x27; &amp;&amp; repo?.options?.expirationDate" v-text="fmt?.humanTime(repo?.options?.expirationDate)"></span>
<span class="expires-past" ng-if="repo.status == 'expired'">Expired<span ng-if="repo.options.expirationDate"> {{repo.options.expirationDate | humanTime}}</span></span> <span class="expires-past" v-if="repo?.status == &#x27;expired&#x27;">Expired<span v-if="repo?.options?.expirationDate"> {{ fmt?.humanTime(repo?.options?.expirationDate) }}</span></span>
<span class="empty-dash" ng-if="repo.status != 'ready' && repo.status != 'expired'" aria-label="Not applicable">&mdash;</span> <span class="empty-dash" aria-label="Not applicable" v-if="repo?.status != &#x27;ready&#x27; &amp;&amp; repo?.status != &#x27;expired&#x27;">&mdash;</span>
</div> </div>
<div class="cell-actions" role="cell"> <div class="cell-actions" role="cell">
<div class="dropdown"> <div class="dropdown">
<button <button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label="&quot;Actions for &quot; + (repo?.repoId)">
class="btn btn-icon-dots"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
aria-label="Actions for {{repo.repoId}}"
>
<i class="fas fa-ellipsis-h" aria-hidden="true"></i> <i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/r/{{repo.repoId}}/"> <a class="dropdown-item" :href="safeUrl(&quot;/r/&quot; + (repo?.repoId) + &quot;/&quot;)">
<i class="fa fa-eye" aria-hidden="true"></i> View <i class="fa fa-eye" aria-hidden="true"></i> View
</a> </a>
<a class="dropdown-item" href="/anonymize/{{repo.repoId}}"> <a class="dropdown-item" :href="safeUrl(&quot;/anonymize/&quot; + (repo?.repoId))">
<i class="far fa-edit" aria-hidden="true"></i> Edit <i class="far fa-edit" aria-hidden="true"></i> Edit
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="paper-table-empty" ng-if="filteredRepositories.length == 0"> <div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
<i class="fas fa-inbox" aria-hidden="true"></i> <i class="fas fa-inbox" aria-hidden="true"></i>
<span ng-if="!conference.repositories.length">No repository has been submitted to this conference yet.</span> <span v-if="!conference?.repositories?.length">No repository has been submitted to this conference yet.</span>
<span ng-if="conference.repositories.length">Nothing matches the current filters.</span> <span v-if="conference?.repositories?.length">Nothing matches the current filters.</span>
<div class="paper-table-empty-actions" ng-if="conference.repositories.length"> <div class="paper-table-empty-actions" v-if="conference?.repositories?.length">
<button type="button" class="btn btn-outline-ink" ng-click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button> <button type="button" class="btn btn-outline-ink" @click="search = &#x27;&#x27;; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
</div> </div>
</div> </div>
</div> </div>
+38 -72
View File
@@ -12,68 +12,46 @@
</a> </a>
</div> </div>
<form class="w-100 dashboard-filter-row" aria-label="Filter conferences" accept-charset="UTF-8" ng-submit="$event.preventDefault()"> <form class="w-100 dashboard-filter-row" aria-label="Filter conferences" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () =&gt; { $event.preventDefault() })">
<div class="search-wrap"> <div class="search-wrap">
<input <input type="search" id="search" class="form-control" aria-label="Search conferences" placeholder="Search by name or ID…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { search = value }, value: search, form: null, options: {} }">
type="search"
id="search"
class="form-control"
aria-label="Search conferences"
placeholder="Search by name or ID…"
autocomplete="off"
ng-model="search"
/>
</div> </div>
<div class="dashboard-filter-controls"> <div class="dashboard-filter-controls">
<div class="dropdown"> <div class="dropdown">
<button <button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn dropdown-toggle"
type="button"
id="dropdownSort"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="filter-btn-label">Sort</span> <span class="filter-btn-label">Sort</span>
<span class="filter-btn-value">{{ ({name: 'Name', conferenceID: 'ID', '-status': 'Status'})[orderBy] || 'Custom' }}</span> <span class="filter-btn-value">{{ ({name: 'Name', conferenceID: 'ID', '-status': 'Status'})[orderBy] || 'Custom' }}</span>
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownSort"> <div class="dropdown-menu" aria-labelledby="dropdownSort">
<h6 class="dropdown-header">Order by</h6> <h6 class="dropdown-header">Order by</h6>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortName" value="name" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortName" value="name" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortName">Name</label> <label class="form-check-label" for="sortName">Name</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortID" value="conferenceID" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortID" value="conferenceID" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortID">ID</label> <label class="form-check-label" for="sortID">ID</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortStatus">Status</label> <label class="form-check-label" for="sortStatus">Status</label>
</div> </div>
</div> </div>
</div> </div>
<div class="dropdown" ng-init="statusLabels = {ready: 'Ready', expired: 'Expired', removed: 'Removed'}"> <div class="dropdown">
<button <button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn dropdown-toggle"
type="button"
id="dropdownStatus"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="filter-btn-label">Status</span> <span class="filter-btn-label">Status</span>
<span class="filter-btn-value" ng-if="filters.status.ready && filters.status.expired && filters.status.removed">All</span> <span class="filter-btn-value" v-if="filters?.status?.ready &amp;&amp; filters?.status?.expired &amp;&amp; filters?.status?.removed">All</span>
<span class="filter-btn-value" ng-if="!(filters.status.ready && filters.status.expired && filters.status.removed)"> <span class="filter-btn-value" v-if="!(filters?.status?.ready &amp;&amp; filters?.status?.expired &amp;&amp; filters?.status?.removed)">
{{ (filters.status.ready ? 0 : 1) + (filters.status.expired ? 0 : 1) + (filters.status.removed ? 0 : 1) }} hidden {{ (filters?.status?.ready ? 0 : 1) + (filters?.status?.expired ? 0 : 1) + (filters?.status?.removed ? 0 : 1) }} hidden
</span> </span>
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownStatus"> <div class="dropdown-menu" aria-labelledby="dropdownStatus">
<h6 class="dropdown-header">Show statuses</h6> <h6 class="dropdown-header">Show statuses</h6>
<div class="form-check dropdown-item" ng-repeat="(key, label) in statusLabels"> <div class="form-check dropdown-item" v-for="(label, key, index) in statusLabels">
<input class="form-check-input" type="checkbox" id="status-{{key}}" ng-model="filters.status[key]" /> <input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value =&gt; { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id="&quot;status-&quot; + (key)">
<label class="form-check-label" for="status-{{key}}">{{label}}</label> <label class="form-check-label" :for="&quot;status-&quot; + (key)">{{ label }}</label>
</div> </div>
</div> </div>
</div> </div>
@@ -82,13 +60,13 @@
<div class="dashboard-meta"> <div class="dashboard-meta">
<span class="dashboard-count"> <span class="dashboard-count">
<span ng-if="filteredConferences.length === conferences.length">{{conferences.length | number}} conference<span ng-if="conferences.length !== 1">s</span></span> <span v-if="filteredConferences?.length === conferences?.length">{{ fmt?.number(conferences?.length) }} conference<span v-if="conferences?.length !== 1">s</span></span>
<span ng-if="filteredConferences.length !== conferences.length">{{filteredConferences.length | number}} of {{conferences.length | number}} shown</span> <span v-if="filteredConferences?.length !== conferences?.length">{{ fmt?.number(filteredConferences?.length) }} of {{ fmt?.number(conferences?.length) }} shown</span>
</span>
<span class="filter-chip" ng-repeat="(f, v) in filters.status" ng-if="v === false">
Hiding {{statusLabels[f]}}
<button type="button" class="filter-chip-close" aria-label="Show {{statusLabels[f]}} again" ng-click="filters.status[f] = true;">&times;</button>
</span> </span>
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
Hiding {{ statusLabels[f] }}
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label="&quot;Show &quot; + (statusLabels[f]) + &quot; again&quot;">&times;</button>
</span></template>
</div> </div>
</div> </div>
@@ -100,54 +78,42 @@
<div role="columnheader">Review window</div> <div role="columnheader">Review window</div>
<div role="columnheader"><span class="sr-only">Actions</span></div> <div role="columnheader"><span class="sr-only">Actions</span></div>
</div> </div>
<div <div class="paper-table-row row-clickable" role="row" v-for="(conference, index) in filteredConferences" :key="conference?.conferenceID" :class="{&#x27;repo-inactive&#x27;: conference?.status == &#x27;expired&#x27; || conference?.status == &#x27;removed&#x27;}">
class="paper-table-row row-clickable"
role="row"
ng-class="{'repo-inactive': conference.status == 'expired' || conference.status == 'removed'}"
ng-repeat="conference in conferences | filter:conferenceFilter | orderBy:orderBy as filteredConferences track by conference.conferenceID"
>
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<span class="type-badge type-repo">Conf</span> <span class="type-badge type-repo">Conf</span>
<div class="anon-text"> <div class="anon-text">
<a class="repo-name" ng-href="/conference/{{conference.conferenceID}}" ng-bind="conference.name"></a> <a class="repo-name" v-text="conference?.name" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID))"></a>
<div class="anon-sub"> <div class="anon-sub">
<span class="anon-source">ID <span class="commit-hash" ng-bind="conference.conferenceID"></span><span ng-if="conference.url"> &middot; <a ng-href="{{conference.url}}" target="_blank" rel="noopener" ng-bind="conference.url | limitTo: 60"></a></span></span> <span class="anon-source">ID <span class="commit-hash" v-text="conference?.conferenceID"></span><span v-if="conference?.url"> &middot; <a target="_blank" rel="noopener" v-text="fmt?.limitTo(conference?.url, 60)" :href="safeUrl((conference?.url))"></a></span></span>
</div> </div>
</div> </div>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<div class="status-line"> <div class="status-line">
<span class="status-dot" ng-class="'status-' + conference.status" aria-hidden="true"></span> <span class="status-dot" aria-hidden="true" :class="&#x27;status-&#x27; + conference?.status"></span>
<span class="status-word" ng-bind="conference.status | statusLabel"></span> <span class="status-word" v-text="fmt?.statusLabel(conference?.status)"></span>
</div> </div>
</div> </div>
<div class="cell-views num" role="cell" ng-bind="::conference.nbRepositories || 0 | number"></div> <div class="cell-views num" role="cell" v-text="fmt?.number(conference?.nbRepositories || 0)"></div>
<div class="cell-expires" role="cell"> <div class="cell-expires" role="cell">
<span ng-if="conference.startDate">{{conference.startDate | date:'mediumDate'}} &ndash; {{conference.endDate | date:'mediumDate'}}</span> <span v-if="conference?.startDate">{{ fmt?.date(conference?.startDate, 'mediumDate') }} &ndash; {{ fmt?.date(conference?.endDate, 'mediumDate') }}</span>
<span class="empty-dash" ng-if="!conference.startDate" aria-label="No review window">&mdash;</span> <span class="empty-dash" aria-label="No review window" v-if="!conference?.startDate">&mdash;</span>
</div> </div>
<div class="cell-actions" role="cell"> <div class="cell-actions" role="cell">
<div class="dropdown"> <div class="dropdown">
<button <button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label="&quot;Actions for &quot; + (conference?.name)">
class="btn btn-icon-dots"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
aria-label="Actions for {{conference.name}}"
>
<i class="fas fa-ellipsis-h" aria-hidden="true"></i> <i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/conference/{{conference.conferenceID}}/"> <a class="dropdown-item" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/&quot;)">
<i class="fa fa-eye" aria-hidden="true"></i> View <i class="fa fa-eye" aria-hidden="true"></i> View
</a> </a>
<a class="dropdown-item" href="/conference/{{conference.conferenceID}}/edit"> <a class="dropdown-item" :href="safeUrl(&quot;/conference/&quot; + (conference?.conferenceID) + &quot;/edit&quot;)">
<i class="far fa-edit" aria-hidden="true"></i> Edit <i class="far fa-edit" aria-hidden="true"></i> Edit
</a> </a>
<div ng-if="conference.status != 'removed'"> <div v-if="conference?.status != &#x27;removed&#x27;">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<button type="button" class="dropdown-item dropdown-item-danger" ng-click="removeConference(conference)"> <button type="button" class="dropdown-item dropdown-item-danger" @click="removeConference(conference)">
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove <i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
</button> </button>
</div> </div>
@@ -155,13 +121,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="paper-table-empty" ng-if="filteredConferences.length == 0"> <div class="paper-table-empty" v-if="filteredConferences?.length == 0">
<i class="fas fa-inbox" aria-hidden="true"></i> <i class="fas fa-inbox" aria-hidden="true"></i>
<span ng-if="conferences.length == 0">You have not created a conference yet.</span> <span v-if="conferences?.length == 0">You have not created a conference yet.</span>
<span ng-if="conferences.length > 0">Nothing matches the current filters.</span> <span v-if="conferences?.length &gt; 0">Nothing matches the current filters.</span>
<div class="paper-table-empty-actions"> <div class="paper-table-empty-actions">
<button type="button" class="btn btn-outline-ink" ng-if="conferences.length > 0" ng-click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button> <button type="button" class="btn btn-outline-ink" v-if="conferences?.length &gt; 0" @click="search = &#x27;&#x27;; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
<a href="/conference/new" class="btn btn-ink" ng-if="conferences.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New conference</a> <a href="/conference/new" class="btn btn-ink" v-if="conferences?.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New conference</a>
</div> </div>
</div> </div>
</div> </div>
+100 -145
View File
@@ -13,105 +13,74 @@
</div> </div>
<!-- Quota --> <!-- Quota -->
<div class="quota-row" ng-if="quota"> <div class="quota-row" v-if="quota">
<div class="quota-item" ng-repeat="q in [ <div class="quota-item" v-for="(q, index) in [
{key: 'repository', label: 'Repositories', kind: 'count'}, {key: &#x27;repository&#x27;, label: &#x27;Repositories&#x27;, kind: &#x27;count&#x27;},
{key: 'storage', label: 'Storage', kind: 'bytes'}, {key: &#x27;storage&#x27;, label: &#x27;Storage&#x27;, kind: &#x27;bytes&#x27;},
{key: 'file', label: 'Files', kind: 'count'} {key: &#x27;file&#x27;, label: &#x27;Files&#x27;, kind: &#x27;count&#x27;}
]"> ]">
<div class="quota-header"> <div class="quota-header">
<span class="quota-label">{{q.label}}</span> <span class="quota-label">{{ q?.label }}</span>
<span class="quota-value" ng-if="q.kind === 'count'"> <span class="quota-value" v-if="q?.kind === &#x27;count&#x27;">
{{quota[q.key].used | number}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | number}}</span> {{ fmt?.number(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.number(quota[q?.key].total) }}</span>
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span> <span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
</span> </span>
<span class="quota-value" ng-if="q.kind === 'bytes'"> <span class="quota-value" v-if="q?.kind === &#x27;bytes&#x27;">
{{quota[q.key].used | humanFileSize}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | humanFileSize}}</span> {{ fmt?.humanFileSize(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.humanFileSize(quota[q?.key].total) }}</span>
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span> <span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
</span> </span>
</div> </div>
<div <div class="quota-track" role="progressbar" aria-valuemin="0" :class="&#x27;quota-&#x27; + quota[q?.key].level" :aria-label="(q?.label) + &quot; quota&quot;" :aria-valuenow="(quota[q?.key].used)" :aria-valuemax="(quota[q?.key].unlimited ? quota[q?.key].used : quota[q?.key].total)" :aria-valuetext="(quota[q?.key].unlimited ? &#x27;unlimited&#x27; : fmt.number(quota[q?.key].percent, 0) + &#x27;% used&#x27;)">
class="quota-track" <div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + &#x27;%&#x27;}"></div>
ng-class="'quota-' + quota[q.key].level"
role="progressbar"
aria-label="{{q.label}} quota"
aria-valuenow="{{quota[q.key].used}}"
aria-valuemin="0"
aria-valuemax="{{quota[q.key].unlimited ? quota[q.key].used : quota[q.key].total}}"
aria-valuetext="{{quota[q.key].unlimited ? 'unlimited' : (quota[q.key].percent | number:0) + '% used'}}"
>
<div class="quota-fill" ng-if="!quota[q.key].unlimited" ng-style="{width: quota[q.key].percent + '%'}"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- Search + filters --> <!-- Search + filters -->
<form class="w-100 dashboard-filter-row" aria-label="Filter anonymizations" accept-charset="UTF-8" ng-submit="$event.preventDefault()"> <form class="w-100 dashboard-filter-row" aria-label="Filter anonymizations" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () =&gt; { $event.preventDefault() })">
<div class="search-wrap"> <div class="search-wrap">
<input <input type="search" id="search" class="form-control" aria-label="Search anonymizations" placeholder="Search by name, source, or conference…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { search = value }, value: search, form: null, options: {} }">
type="search"
id="search"
class="form-control"
aria-label="Search anonymizations"
placeholder="Search by name, source, or conference…"
autocomplete="off"
ng-model="search"
/>
</div> </div>
<div class="dashboard-filter-controls"> <div class="dashboard-filter-controls">
<div class="btn-group" role="group" aria-label="Type"> <div class="btn-group" role="group" aria-label="Type">
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'all'}" aria-pressed="{{typeFilter === 'all'}}" ng-click="typeFilter = 'all'">All</button> <button type="button" class="btn" :class="{&#x27;btn-primary&#x27;: typeFilter === &#x27;all&#x27;}" @click="typeFilter = &#x27;all&#x27;" :aria-pressed="(typeFilter === &#x27;all&#x27;)">All</button>
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'repo'}" aria-pressed="{{typeFilter === 'repo'}}" ng-click="typeFilter = 'repo'">Repos</button> <button type="button" class="btn" :class="{&#x27;btn-primary&#x27;: typeFilter === &#x27;repo&#x27;}" @click="typeFilter = &#x27;repo&#x27;" :aria-pressed="(typeFilter === &#x27;repo&#x27;)">Repos</button>
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'pr'}" aria-pressed="{{typeFilter === 'pr'}}" ng-click="typeFilter = 'pr'">PRs</button> <button type="button" class="btn" :class="{&#x27;btn-primary&#x27;: typeFilter === &#x27;pr&#x27;}" @click="typeFilter = &#x27;pr&#x27;" :aria-pressed="(typeFilter === &#x27;pr&#x27;)">PRs</button>
<button type="button" class="btn" ng-class="{'btn-primary': typeFilter === 'gist'}" aria-pressed="{{typeFilter === 'gist'}}" ng-click="typeFilter = 'gist'">Gists</button> <button type="button" class="btn" :class="{&#x27;btn-primary&#x27;: typeFilter === &#x27;gist&#x27;}" @click="typeFilter = &#x27;gist&#x27;" :aria-pressed="(typeFilter === &#x27;gist&#x27;)">Gists</button>
</div> </div>
<div class="dropdown"> <div class="dropdown">
<button <button class="btn dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn dropdown-toggle"
type="button"
id="dropdownSort"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="filter-btn-label">Sort</span> <span class="filter-btn-label">Sort</span>
<span class="filter-btn-value">{{sortLabel()}}</span> <span class="filter-btn-value">{{ sortLabel() }}</span>
<i class="fas" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i> <i class="fas" aria-hidden="true" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></i>
<span class="sr-only">{{sortDesc() ? 'descending' : 'ascending'}}</span> <span class="sr-only">{{ sortDesc() ? 'descending' : 'ascending' }}</span>
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownSort"> <div class="dropdown-menu" aria-labelledby="dropdownSort">
<h6 class="dropdown-header">Order by</h6> <h6 class="dropdown-header">Order by</h6>
<div class="form-check dropdown-item" ng-repeat="(field, def) in sortFields"> <div class="form-check dropdown-item" v-for="(def, field, index) in sortFields">
<input class="form-check-input" type="radio" name="sort" id="sort-{{$index}}" ng-checked="isSortedBy(field)" ng-click="setSort(field, def.defaultDesc)" /> <input class="form-check-input" type="radio" name="sort" @click="setSort(field, def.defaultDesc)" :checked="isSortedBy(field)" :id="&quot;sort-&quot; + (index)">
<label class="form-check-label" for="sort-{{$index}}">{{def.label}}</label> <label class="form-check-label" :for="&quot;sort-&quot; + (index)">{{ def?.label }}</label>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<button type="button" class="dropdown-item" ng-click="toggleSortDirection()"> <button type="button" class="dropdown-item" @click="toggleSortDirection()">
<i class="fas fa-exchange-alt fa-rotate-90" aria-hidden="true"></i> <i class="fas fa-exchange-alt fa-rotate-90" aria-hidden="true"></i>
{{sortDesc() ? 'Switch to ascending' : 'Switch to descending'}} {{ sortDesc() ? 'Switch to ascending' : 'Switch to descending' }}
</button> </button>
</div> </div>
</div> </div>
<div class="dropdown"> <div class="dropdown">
<button <button class="btn dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn dropdown-toggle"
type="button"
id="dropdownStatus"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="filter-btn-label">Status</span> <span class="filter-btn-label">Status</span>
<span class="filter-btn-value" ng-if="!hasHiddenStatus()">All</span> <span class="filter-btn-value" v-if="!hasHiddenStatus()">All</span>
<span class="filter-btn-value" ng-if="hasHiddenStatus()">{{hiddenStatusCount()}} hidden</span> <span class="filter-btn-value" v-if="hasHiddenStatus()">{{ hiddenStatusCount() }} hidden</span>
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownStatus"> <div class="dropdown-menu" aria-labelledby="dropdownStatus">
<h6 class="dropdown-header">Show statuses</h6> <h6 class="dropdown-header">Show statuses</h6>
<div class="form-check dropdown-item" ng-repeat="(key, label) in statusKeyLabels"> <div class="form-check dropdown-item" v-for="(label, key, index) in statusKeyLabels">
<input class="form-check-input" type="checkbox" id="status-{{key}}" ng-model="filters.status[key]" /> <input class="form-check-input" type="checkbox" v-field="{ state: viewState, set: value =&gt; { filters.status[key] = value }, value: filters?.status[key], form: null, options: {} }" :id="&quot;status-&quot; + (key)">
<label class="form-check-label" for="status-{{key}}">{{label}}</label> <label class="form-check-label" :for="&quot;status-&quot; + (key)">{{ label }}</label>
</div> </div>
</div> </div>
</div> </div>
@@ -119,154 +88,140 @@
</form> </form>
<!-- Result count + active filter chips --> <!-- Result count + active filter chips -->
<div class="dashboard-meta" ng-if="!loading"> <div class="dashboard-meta" v-if="!loading">
<span class="dashboard-count"> <span class="dashboard-count">
<span ng-if="filteredItems.length === items.length">{{items.length | number}} anonymization<span ng-if="items.length !== 1">s</span></span> <span v-if="filteredItems?.length === items?.length">{{ fmt?.number(items?.length) }} anonymization<span v-if="items?.length !== 1">s</span></span>
<span ng-if="filteredItems.length !== items.length">{{filteredItems.length | number}} of {{items.length | number}} shown</span> <span v-if="filteredItems?.length !== items?.length">{{ fmt?.number(filteredItems?.length) }} of {{ fmt?.number(items?.length) }} shown</span>
</span> </span>
<span class="filter-chip" ng-repeat="(f, v) in filters.status" ng-if="v === false"> <template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
Hiding {{statusKeyLabels[f]}} Hiding {{ statusKeyLabels[f] }}
<button type="button" class="filter-chip-close" aria-label="Show {{statusKeyLabels[f]}} again" ng-click="filters.status[f] = true;">&times;</button> <button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label="&quot;Show &quot; + (statusKeyLabels[f]) + &quot; again&quot;">&times;</button>
</span> </span></template>
<button type="button" class="btn-link-inline" ng-if="hasActiveFilters()" ng-click="clearFilters()">Clear filters</button> <button type="button" class="btn-link-inline" v-if="hasActiveFilters()" @click="clearFilters()">Clear filters</button>
</div> </div>
</div> </div>
<!-- Table --> <!-- Table -->
<div class="paper-table paper-table-dashboard w-100" role="table" aria-label="Anonymizations" aria-busy="{{loading}}"> <div class="paper-table paper-table-dashboard w-100" role="table" aria-label="Anonymizations" :aria-busy="(loading)">
<div class="paper-table-head" role="row"> <div class="paper-table-head" role="row">
<div role="columnheader" aria-sort="{{isSortedBy('_name') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}"> <div role="columnheader" :aria-sort="(isSortedBy(&#x27;_name&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" ng-class="{active: isSortedBy('_name')}" ng-click="setSort('_name')"> <button type="button" class="sortable" :class="{active: isSortedBy(&#x27;_name&#x27;)}" @click="setSort(&#x27;_name&#x27;)">
Anonymization <i class="fas" ng-if="isSortedBy('_name')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i> Anonymization <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;_name&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></i>
</button> </button>
</div> </div>
<div role="columnheader" aria-sort="{{isSortedBy('status') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}"> <div role="columnheader" :aria-sort="(isSortedBy(&#x27;status&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" ng-class="{active: isSortedBy('status')}" ng-click="setSort('status')"> <button type="button" class="sortable" :class="{active: isSortedBy(&#x27;status&#x27;)}" @click="setSort(&#x27;status&#x27;)">
Status <i class="fas" ng-if="isSortedBy('status')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i> Status <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;status&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></i>
</button> </button>
</div> </div>
<div role="columnheader" class="num" aria-sort="{{isSortedBy('pageView') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}"> <div role="columnheader" class="num" :aria-sort="(isSortedBy(&#x27;pageView&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" ng-class="{active: isSortedBy('pageView')}" ng-click="setSort('pageView')"> <button type="button" class="sortable" :class="{active: isSortedBy(&#x27;pageView&#x27;)}" @click="setSort(&#x27;pageView&#x27;)">
Views <i class="fas" ng-if="isSortedBy('pageView')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i> Views <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;pageView&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></i>
</button> </button>
</div> </div>
<div role="columnheader" aria-sort="{{isSortedBy('options.expirationDate') ? (sortDesc() ? 'descending' : 'ascending') : 'none'}}"> <div role="columnheader" :aria-sort="(isSortedBy(&#x27;options.expirationDate&#x27;) ? (sortDesc() ? &#x27;descending&#x27; : &#x27;ascending&#x27;) : &#x27;none&#x27;)">
<button type="button" class="sortable" ng-class="{active: isSortedBy('options.expirationDate')}" ng-click="setSort('options.expirationDate')"> <button type="button" class="sortable" :class="{active: isSortedBy(&#x27;options.expirationDate&#x27;)}" @click="setSort(&#x27;options.expirationDate&#x27;)">
Expires <i class="fas" ng-if="isSortedBy('options.expirationDate')" ng-class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'" aria-hidden="true"></i> Expires <i class="fas" aria-hidden="true" v-if="isSortedBy(&#x27;options.expirationDate&#x27;)" :class="sortDesc() ? &#x27;fa-arrow-down&#x27; : &#x27;fa-arrow-up&#x27;"></i>
</button> </button>
</div> </div>
<div role="columnheader"><span class="sr-only">Actions</span></div> <div role="columnheader"><span class="sr-only">Actions</span></div>
</div> </div>
<!-- Loading skeleton: keeps the layout stable while the three lists load --> <!-- Loading skeleton: keeps the layout stable while the three lists load -->
<div class="paper-table-row paper-table-skeleton" role="row" aria-hidden="true" ng-if="loading" ng-repeat="i in [1, 2, 3, 4]"> <template v-for="(i, index) in [1, 2, 3, 4]"><div class="paper-table-row paper-table-skeleton" role="row" aria-hidden="true" v-if="loading">
<div class="cell-anon" role="cell"><span class="skeleton skeleton-badge"></span><div class="anon-text"><span class="skeleton skeleton-line" style="width: 38%"></span><span class="skeleton skeleton-line skeleton-line-sm" style="width: 56%"></span></div></div> <div class="cell-anon" role="cell"><span class="skeleton skeleton-badge"></span><div class="anon-text"><span class="skeleton skeleton-line" style="width: 38%"></span><span class="skeleton skeleton-line skeleton-line-sm" style="width: 56%"></span></div></div>
<div class="cell-status" role="cell"><span class="skeleton skeleton-line" style="width: 60%"></span></div> <div class="cell-status" role="cell"><span class="skeleton skeleton-line" style="width: 60%"></span></div>
<div class="cell-views num" role="cell"><span class="skeleton skeleton-line" style="width: 40%"></span></div> <div class="cell-views num" role="cell"><span class="skeleton skeleton-line" style="width: 40%"></span></div>
<div class="cell-expires" role="cell"><span class="skeleton skeleton-line" style="width: 55%"></span></div> <div class="cell-expires" role="cell"><span class="skeleton skeleton-line" style="width: 55%"></span></div>
<div class="cell-actions" role="cell"></div> <div class="cell-actions" role="cell"></div>
</div> </div></template>
<div <template v-for="(item, index) in filteredItems" :key="(item?._type + &#x27;:&#x27; + (item?._id || index))"><div class="paper-table-row" role="row" v-if="!loading" :class="{&#x27;repo-inactive&#x27;: item?._statusKey == &#x27;expired&#x27; || item?._statusKey == &#x27;removed&#x27;, &#x27;repo-error&#x27;: item?.status == &#x27;error&#x27;, &#x27;row-clickable&#x27;: !!item?._viewUrl}" @click="openItem(item, $event)">
class="paper-table-row"
role="row"
ng-if="!loading"
ng-class="{'repo-inactive': item._statusKey == 'expired' || item._statusKey == 'removed', 'repo-error': item.status == 'error', 'row-clickable': !!item._viewUrl}"
ng-repeat="item in items | filter:itemFilter | orderBy:orderBy as filteredItems track by (item._type + ':' + (item._id || $index))"
ng-click="openItem(item, $event)"
>
<div class="cell-anon" role="cell"> <div class="cell-anon" role="cell">
<span class="type-badge" ng-class="{'type-repo': item._type === 'repo', 'type-pr': item._type === 'pr', 'type-gist': item._type === 'gist'}">{{item._type === 'repo' ? 'Repo' : item._type === 'pr' ? 'PR' : 'Gist'}}</span> <span class="type-badge" :class="{&#x27;type-repo&#x27;: item?._type === &#x27;repo&#x27;, &#x27;type-pr&#x27;: item?._type === &#x27;pr&#x27;, &#x27;type-gist&#x27;: item?._type === &#x27;gist&#x27;}">{{ item?._type === 'repo' ? 'Repo' : item?._type === 'pr' ? 'PR' : 'Gist' }}</span>
<div class="anon-text"> <div class="anon-text">
<div class="anon-title"> <div class="anon-title">
<a ng-if="item._viewUrl" ng-href="{{item._viewUrl}}" class="repo-name" ng-bind="item._name"></a> <a class="repo-name" v-if="item?._viewUrl" v-text="item?._name" :href="safeUrl((item?._viewUrl))"></a>
<span ng-if="!item._viewUrl" class="repo-name repo-name-static" ng-bind="item._name"></span> <span class="repo-name repo-name-static" v-if="!item?._viewUrl" v-text="item?._name"></span>
<span class="type-badge type-coauthor" ng-if="item.role === 'coauthor'" title="You are a co-author on this anonymization">Co-author</span> <span class="type-badge type-coauthor" title="You are a co-author on this anonymization" v-if="item?.role === &#x27;coauthor&#x27;">Co-author</span>
</div> </div>
<div class="anon-sub" ng-if="!item._broken || item.conference"> <div class="anon-sub" v-if="!item?._broken || item?.conference">
<span class="anon-source" ng-if="item._type === 'repo' && item.source.fullName && !item._broken"> <span class="anon-source" v-if="item?._type === &#x27;repo&#x27; &amp;&amp; item?.source?.fullName &amp;&amp; !item?._broken">
<a href="https://github.com/{{item.source.fullName}}/" target="_blank" rel="noopener" ng-bind="item.source.fullName"></a><span ng-if="item.options.update && item.source.branch"> &middot; <a href="https://github.com/{{item.source.fullName}}/tree/{{item.source.branch}}" target="_blank" rel="noopener" ng-bind="item.source.branch"></a></span><span ng-if="item.source.commit"> &middot; <a class="commit-hash" href="https://github.com/{{item.source.fullName}}/tree/{{item.source.commit}}" target="_blank" rel="noopener">@{{item.source.commit.substring(0, 8)}}</a></span> <a target="_blank" rel="noopener" v-text="item?.source?.fullName" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.fullName) + &quot;/&quot;)"></a><span v-if="item?.options?.update &amp;&amp; item?.source?.branch"> &middot; <a target="_blank" rel="noopener" v-text="item?.source?.branch" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.fullName) + &quot;/tree/&quot; + (item?.source?.branch))"></a></span><span v-if="item?.source?.commit"> &middot; <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.fullName) + &quot;/tree/&quot; + (item?.source?.commit))">@{{ item?.source?.commit?.substring(0, 8) }}</a></span>
</span> </span>
<span class="anon-source" ng-if="item._type === 'pr' && item.source.repositoryFullName && !item._broken"> <span class="anon-source" v-if="item?._type === &#x27;pr&#x27; &amp;&amp; item?.source?.repositoryFullName &amp;&amp; !item?._broken">
<a href="https://github.com/{{item.source.repositoryFullName}}/pull/{{item.source.pullRequestId}}" target="_blank" rel="noopener" ng-bind="item._source"></a> <a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl(&quot;https://github.com/&quot; + (item?.source?.repositoryFullName) + &quot;/pull/&quot; + (item?.source?.pullRequestId))"></a>
</span> </span>
<span class="anon-source" ng-if="item._type === 'gist' && item.source.gistId && !item._broken"> <span class="anon-source" v-if="item?._type === &#x27;gist&#x27; &amp;&amp; item?.source?.gistId &amp;&amp; !item?._broken">
<a href="https://gist.github.com/{{item.source.gistId}}" target="_blank" rel="noopener" ng-bind="item._source"></a> <a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl(&quot;https://gist.github.com/&quot; + (item?.source?.gistId))"></a>
</span> </span>
<span class="cell-conf conf-tag" ng-if="item.conference" title="{{item.conference}}"><i class="fas fa-university" aria-hidden="true"></i> <span ng-bind="item.conference"></span></span> <span class="cell-conf conf-tag" v-if="item?.conference" :title="(item?.conference)"><i class="fas fa-university" aria-hidden="true"></i> <span v-text="item?.conference"></span></span>
</div> </div>
</div> </div>
</div> </div>
<div class="cell-status" role="cell"> <div class="cell-status" role="cell">
<div class="status-line"> <div class="status-line">
<span class="status-dot" ng-class="'status-' + item._statusKey" aria-hidden="true"></span> <span class="status-dot" aria-hidden="true" :class="&#x27;status-&#x27; + item?._statusKey"></span>
<span class="status-word" ng-bind="item.status | statusLabel"></span> <span class="status-word" v-text="fmt?.statusLabel(item?.status)"></span>
</div> </div>
<div class="status-sub status-sub-error" ng-if="item.status == 'error' && item.statusMessage" title="{{item.statusMessage}}" ng-bind="item.statusMessage | statusMsg"></div> <div class="status-sub status-sub-error" v-if="item?.status == &#x27;error&#x27; &amp;&amp; item?.statusMessage" v-text="fmt?.statusMsg(item?.statusMessage)" :title="(item?.statusMessage)"></div>
<div class="status-sub status-sub-warn" ng-if="item._stale" title="Last activity {{item.anonymizeDate || item.lastView | humanTime}}"> <div class="status-sub status-sub-warn" v-if="item?._stale" :title="&quot;Last activity &quot; + (fmt?.humanTime(item?.anonymizeDate || item?.lastView))">
Last activity {{item.anonymizeDate || item.lastView | humanTime}} &middot; may be stuck Last activity {{ fmt?.humanTime(item?.anonymizeDate || item?.lastView) }} &middot; may be stuck
</div> </div>
<div class="status-sub" ng-if="item.status != 'error' && !item._stale && item.anonymizeDate"> <div class="status-sub" v-if="item?.status != &#x27;error&#x27; &amp;&amp; !item?._stale &amp;&amp; item?.anonymizeDate">
anonymized {{item.anonymizeDate | humanTime}} anonymized {{ fmt?.humanTime(item?.anonymizeDate) }}
</div> </div>
</div> </div>
<div class="cell-views num" role="cell" ng-bind="item.pageView | number"></div> <div class="cell-views num" role="cell" v-text="fmt?.number(item?.pageView)"></div>
<div class="cell-expires" role="cell"> <div class="cell-expires" role="cell">
<span ng-if="item._expiry.kind === 'never'" class="expires-never">Never</span> <span class="expires-never" v-if="item?._expiry?.kind === &#x27;never&#x27;">Never</span>
<span ng-if="item._expiry.kind === 'date'" ng-bind="item._expiry.date | humanTime"></span> <span v-if="item?._expiry?.kind === &#x27;date&#x27;" v-text="fmt?.humanTime(item?._expiry?.date)"></span>
<span ng-if="item._expiry.kind === 'expired' && item._expiry.date" class="expires-past">Expired {{item._expiry.date | humanTime}}</span> <span class="expires-past" v-if="item?._expiry?.kind === &#x27;expired&#x27; &amp;&amp; item?._expiry?.date">Expired {{ fmt?.humanTime(item?._expiry?.date) }}</span>
<span ng-if="item._expiry.kind === 'expired' && !item._expiry.date" class="expires-past">Expired</span> <span class="expires-past" v-if="item?._expiry?.kind === &#x27;expired&#x27; &amp;&amp; !item?._expiry?.date">Expired</span>
<span class="empty-dash" ng-if="item._expiry.kind === 'none'" aria-label="Not applicable">&mdash;</span> <span class="empty-dash" aria-label="Not applicable" v-if="item?._expiry?.kind === &#x27;none&#x27;">&mdash;</span>
</div> </div>
<div class="cell-actions" role="cell"> <div class="cell-actions" role="cell">
<div class="dropdown" ng-if="!item._broken"> <div class="dropdown" v-if="!item?._broken">
<button <button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :aria-label="&quot;Actions for &quot; + (item?._name)">
class="btn btn-icon-dots"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
aria-label="Actions for {{item._name}}"
>
<i class="fas fa-ellipsis-h" aria-hidden="true"></i> <i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" ng-href="{{item._viewUrl}}"> <a class="dropdown-item" :href="safeUrl((item?._viewUrl))">
<i class="fa fa-eye" aria-hidden="true"></i> View <i class="fa fa-eye" aria-hidden="true"></i> View
</a> </a>
<a class="dropdown-item" href="/w/{{item.repoId}}/" target="_self" ng-if="item._type === 'repo' && item.options.page && item.status == 'ready'"> <a class="dropdown-item" target="_self" v-if="item?._type === &#x27;repo&#x27; &amp;&amp; item?.options?.page &amp;&amp; item?.status == &#x27;ready&#x27;" :href="safeUrl(&quot;/w/&quot; + (item?.repoId) + &quot;/&quot;)">
<i class="fas fa-globe" aria-hidden="true"></i> View page <i class="fas fa-globe" aria-hidden="true"></i> View page
</a> </a>
<a class="dropdown-item" ng-href="{{item._editUrl}}"> <a class="dropdown-item" :href="safeUrl((item?._editUrl))">
<i class="far fa-edit" aria-hidden="true"></i> Edit <i class="far fa-edit" aria-hidden="true"></i> Edit
</a> </a>
<button type="button" class="dropdown-item" ng-if="item.status == 'ready' || item.status == 'error'" ng-click="refreshItem(item)"> <button type="button" class="dropdown-item" v-if="item?.status == &#x27;ready&#x27; || item?.status == &#x27;error&#x27;" @click="refreshItem(item)">
<i class="fas fa-sync" aria-hidden="true"></i> Force update <i class="fas fa-sync" aria-hidden="true"></i> Force update
</button> </button>
<button type="button" class="dropdown-item" ng-if="item.status == 'removed'" ng-click="refreshItem(item)"> <button type="button" class="dropdown-item" v-if="item?.status == &#x27;removed&#x27;" @click="refreshItem(item)">
<i class="fas fa-check-circle" aria-hidden="true"></i> Enable <i class="fas fa-check-circle" aria-hidden="true"></i> Enable
</button> </button>
<button type="button" class="dropdown-item" ng-if="item.status == 'expired'" ng-click="extendItem(item)"> <button type="button" class="dropdown-item" v-if="item?.status == &#x27;expired&#x27;" @click="extendItem(item)">
<i class="fas fa-calendar-plus" aria-hidden="true"></i> Extend 6 months <i class="fas fa-calendar-plus" aria-hidden="true"></i> Extend 6 months
</button> </button>
<div ng-if="(item.status == 'ready' || item.status == 'expired' || item.status == 'error') && item.role !== 'coauthor'"> <div v-if="(item?.status == &#x27;ready&#x27; || item?.status == &#x27;expired&#x27; || item?.status == &#x27;error&#x27;) &amp;&amp; item?.role !== &#x27;coauthor&#x27;">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<button type="button" class="dropdown-item dropdown-item-danger" ng-click="removeItem(item)"> <button type="button" class="dropdown-item dropdown-item-danger" @click="removeItem(item)">
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove <i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div></template>
<div class="paper-table-empty" ng-if="!loading && filteredItems.length == 0"> <div class="paper-table-empty" v-if="!loading &amp;&amp; filteredItems?.length == 0">
<i class="fas fa-inbox" aria-hidden="true"></i> <i class="fas fa-inbox" aria-hidden="true"></i>
<span ng-if="items.length == 0">You have no anonymizations yet.</span> <span v-if="items?.length == 0">You have no anonymizations yet.</span>
<span ng-if="items.length > 0">Nothing matches the current filters.</span> <span v-if="items?.length &gt; 0">Nothing matches the current filters.</span>
<div class="paper-table-empty-actions"> <div class="paper-table-empty-actions">
<button type="button" class="btn btn-outline-ink" ng-if="hasActiveFilters()" ng-click="clearFilters()">Clear filters</button> <button type="button" class="btn btn-outline-ink" v-if="hasActiveFilters()" @click="clearFilters()">Clear filters</button>
<a href="/anonymize" class="btn btn-ink" ng-if="items.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New anonymization</a> <a href="/anonymize" class="btn btn-ink" v-if="items?.length == 0"><i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New anonymization</a>
</div> </div>
</div> </div>
</div> </div>
+32 -120
View File
@@ -1,159 +1,71 @@
<div class="explorer-page" ng-init="sidebarCollapsed = window.matchMedia && window.matchMedia('(max-width: 767px)').matches"> <div class="explorer-page">
<button <button class="sidebar-toggle" v-show="files?.length" @click="sidebarCollapsed = !sidebarCollapsed" :aria-label="(sidebarCollapsed ? &#x27;Show files&#x27; : &#x27;Hide files&#x27;)">
class="sidebar-toggle" <i class="fas" :class="sidebarCollapsed ? &#x27;fa-folder-open&#x27; : &#x27;fa-times&#x27;"></i>
ng-show="files.length" <span v-text="sidebarCollapsed ? &#x27;Files&#x27; : &#x27;Close&#x27;"></span>
ng-click="sidebarCollapsed = !sidebarCollapsed"
aria-label="{{sidebarCollapsed ? 'Show files' : 'Hide files'}}"
>
<i class="fas" ng-class="sidebarCollapsed ? 'fa-folder-open' : 'fa-times'"></i>
<span ng-bind="sidebarCollapsed ? 'Files' : 'Close'"></span>
</button> </button>
<div class="leftCol" ng-show="files.length" ng-class="{'collapsed': sidebarCollapsed}"> <div class="leftCol" v-show="files?.length" :class="{&#x27;collapsed&#x27;: sidebarCollapsed}">
<div class="leftCol-head"> <div class="leftCol-head">
<span class="leftCol-eyebrow">Files</span> <span class="leftCol-eyebrow">Files</span>
<button class="leftCol-close" ng-click="sidebarCollapsed = true" aria-label="Close files"> <button class="leftCol-close" aria-label="Close files" @click="sidebarCollapsed = true">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="leftCol-search"> <div class="leftCol-search">
<div class="tree-search-box"> <div class="tree-search-box">
<i class="fas tree-search-icon" ng-class="fileSearchLoading ? 'fa-spinner fa-spin' : 'fa-search'"></i> <i class="fas tree-search-icon" :class="fileSearchLoading ? &#x27;fa-spinner fa-spin&#x27; : &#x27;fa-search&#x27;"></i>
<input <input type="text" class="tree-search-input" placeholder="Search files" aria-label="Search files" v-field="{ state: viewState, set: value =&gt; { fileSearchQuery = value }, value: fileSearchQuery, form: null, options: { debounce: 300 }, change: () =&gt; { onFileSearchChange() } }">
type="text" <kbd class="tree-search-kbd" v-show="!(fileSearchQuery)">{{ isMac ? '⌘' : 'Ctrl+' }}K</kbd>
class="tree-search-input" <button class="tree-search-clear" aria-label="Clear search" v-show="fileSearchQuery" @click="fileSearchQuery = &#x27;&#x27;; onFileSearchChange()">&times;</button>
placeholder="Search files"
ng-model="fileSearchQuery"
ng-model-options="{ debounce: 300 }"
ng-change="onFileSearchChange()"
aria-label="Search files"
/>
<kbd class="tree-search-kbd" ng-hide="fileSearchQuery">{{isMac ? '⌘' : 'Ctrl+'}}K</kbd>
<button
class="tree-search-clear"
ng-show="fileSearchQuery"
ng-click="fileSearchQuery = ''; onFileSearchChange()"
aria-label="Clear search"
>&times;</button>
</div> </div>
</div> </div>
<div class="leftCol-project-header"> <div class="leftCol-project-header">
<span class="project-name" ng-bind="repoId"></span> <span class="project-name" v-text="repoId"></span>
<span class="project-file-count">{{fileCounts[''] || files.length}} files</span> <span class="project-file-count">{{ fileCounts?.[''] || files?.length }} files</span>
</div> </div>
<div class="leftCol-body"> <div class="leftCol-body">
<div <div class="paper-inline-warning" role="alert" v-if="options?.truncatedFolders?.length &gt; 0">
ng-if="options.truncatedFolders.length > 0"
class="paper-inline-warning"
role="alert"
>
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
{{ 'WARNINGS.repo_truncated' | translate }} {{ fmt?.translate('WARNINGS.repo_truncated') }}
</div> </div>
<div <div class="paper-inline-warning" role="alert" v-if="options?.hasSubmodules">
ng-if="options.hasSubmodules"
class="paper-inline-warning"
role="alert"
>
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
{{ 'WARNINGS.submodules_not_included' | translate }} {{ fmt?.translate('WARNINGS.submodules_not_included') }}
</div> </div>
<tree class="files" file="files" search-query="fileSearchQuery" search-results="fileSearchResults"></tree> <tree class="files" :file="files" :search-query="fileSearchQuery" :search-results="fileSearchResults" :page="viewState"></tree>
</div> </div>
<div class="leftCol-foot"> <div class="leftCol-foot">
<span <span class="last-update" data-toggle="tooltip" data-placement="top" :title="(options?.lastUpdateDate)">
class="last-update" Updated {{ fmt?.date(options?.lastUpdateDate) }}
data-toggle="tooltip"
data-placement="top"
title="{{options.lastUpdateDate}}"
>
Updated {{options.lastUpdateDate|date}}
</span> </span>
</div> </div>
</div> </div>
<div <div class="leftCol-backdrop" v-show="files?.length &amp;&amp; !sidebarCollapsed" @click="sidebarCollapsed = true"></div>
class="leftCol-backdrop"
ng-show="files.length && !sidebarCollapsed"
ng-click="sidebarCollapsed = true"
></div>
<div class="explorer-main"> <div class="explorer-main">
<div class="status-bar"> <div class="status-bar">
<ol class="breadcrumb paths" aria-label="Path"> <ol class="breadcrumb paths" aria-label="Path">
<li class="breadcrumb-item" ng-repeat="p in paths" ng-bind="p"></li> <li class="breadcrumb-item" v-for="(p, index) in paths" v-text="p"></li>
</ol> </ol>
<div class="status-bar-actions"> <div class="status-bar-actions">
<a <a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl(&quot;/anonymize/&quot; + (repoId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
ng-if="options.isAdmin || options.isOwner" <button class="btn btn-sm" aria-label="Allow this document to run JavaScript" title="Scripts in this file are blocked by default. Enable them only if you trust the repository — the document stays isolated from your session either way." v-if="type == &#x27;html-doc&#x27; &amp;&amp; !showSource" :class="{&#x27;btn-active&#x27;: allowScripts}" @click="toggleAllowScripts()">
ng-href="/anonymize/{{repoId}}"
class="btn btn-sm"
aria-label="Edit"
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
>
<button
ng-if="type == 'html-doc' && !showSource"
ng-click="toggleAllowScripts()"
class="btn btn-sm"
ng-class="{'btn-active': allowScripts}"
aria-label="Allow this document to run JavaScript"
title="Scripts in this file are blocked by default. Enable them only if you trust the repository — the document stays isolated from your session either way."
>
<i class="fab fa-js"></i><span class="d-none d-md-inline"> <i class="fab fa-js"></i><span class="d-none d-md-inline">
{{ allowScripts ? "JS on" : "JS off" }}</span {{ allowScripts ? "JS on" : "JS off" }}</span>
>
</button> </button>
<button <button class="btn btn-sm" aria-label="Toggle between the rendered document and its source" title="Toggle between the rendered document and its source" v-if="type == &#x27;html-doc&#x27;" @click="toggleSource()">
ng-if="type == 'html-doc'" <i class="fas" :class="showSource ? &#x27;fa-eye&#x27; : &#x27;fa-code&#x27;"></i><span class="d-none d-md-inline">
ng-click="toggleSource()" {{ showSource ? "Rendered" : "Source" }}</span>
class="btn btn-sm"
aria-label="Toggle between the rendered document and its source"
title="Toggle between the rendered document and its source"
>
<i class="fas" ng-class="showSource ? 'fa-eye' : 'fa-code'"></i
><span class="d-none d-md-inline">
{{ showSource ? "Rendered" : "Source" }}</span
>
</button> </button>
<a <a target="_self" class="btn btn-sm" aria-label="View raw current file" title="View the raw content of the current file" v-show="content != null" :href="safeUrl((url))"><i class="fas fa-file-alt"></i><span class="d-none d-md-inline"> Raw</span></a>
ng-show="content != null" <a target="_self" class="btn btn-sm" aria-label="Download current file" title="Download the current file" v-show="content != null" :href="safeUrl((url) + &quot;&amp;download=true&quot;)"><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a>
ng-href="{{url}}" <a target="_self" class="btn btn-sm" aria-label="Download full repository as ZIP" title="Download the full repository as a ZIP archive" v-if="options?.download" :href="safeUrl(&quot;/api/repo/&quot; + (repoId) + &quot;/zip&quot;)"><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> Full repo ZIP</span></a>
target="_self" <a target="_self" class="btn btn-sm" aria-label="Website" v-if="options?.hasWebsite" :href="safeUrl(&quot;/w/&quot; + (repoId) + &quot;/&quot;)"><i class="fas fa-globe"></i><span class="d-none d-md-inline"> Website</span></a>
class="btn btn-sm"
aria-label="View raw current file"
title="View the raw content of the current file"
><i class="fas fa-file-alt"></i><span class="d-none d-md-inline"> Raw</span></a
>
<a
ng-show="content != null"
ng-href="{{url}}&download=true"
target="_self"
class="btn btn-sm"
aria-label="Download current file"
title="Download the current file"
><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a
>
<a
ng-if="options.download"
ng-href="/api/repo/{{repoId}}/zip"
target="_self"
class="btn btn-sm"
aria-label="Download full repository as ZIP"
title="Download the full repository as a ZIP archive"
><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> Full repo ZIP</span></a
>
<a
ng-if="options.hasWebsite"
ng-href="/w/{{repoId}}/"
target="_self"
class="btn btn-sm"
aria-label="Website"
><i class="fas fa-globe"></i><span class="d-none d-md-inline"> Website</span></a
>
</div> </div>
</div> </div>
<div class="explorer-content"> <div class="explorer-content">
<ng-include src="'./partials/pageView.htm'"></ng-include> <partial-view name="partials/pageView.htm" :state="viewState"></partial-view>
</div> </div>
</div> </div>
</div> </div>
+37 -271
View File
@@ -25,34 +25,16 @@
<h4 class="faq-cat"> <h4 class="faq-cat">
<i class="fas fa-info-circle mr-2"></i>General <i class="fas fa-info-circle mr-2"></i>General
</h4> </h4>
<div <div class="panel-group" id="faq-general" role="tablist" aria-multiselectable="true">
class="panel-group"
id="faq-general"
role="tablist"
aria-multiselectable="true"
>
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingWhatIs"> <div class="panel-heading p-3" role="tab" id="headingWhatIs">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#whatIs" aria-expanded="false" aria-controls="whatIs">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-general"
href="#whatIs"
aria-expanded="false"
aria-controls="whatIs"
>
What is Anonymous GitHub? What is Anonymous GitHub?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="whatIs" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingWhatIs">
id="whatIs"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingWhatIs"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Anonymous GitHub is a free, open-source tool that anonymizes GitHub repositories and pull requests Anonymous GitHub is a free, open-source tool that anonymizes GitHub repositories and pull requests
@@ -67,25 +49,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingHowWork"> <div class="panel-heading p-3" role="tab" id="headingHowWork">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#howWork" aria-expanded="false" aria-controls="howWork">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-general"
href="#howWork"
aria-expanded="false"
aria-controls="howWork"
>
How does Anonymous GitHub work? How does Anonymous GitHub work?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="howWork" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingHowWork">
id="howWork"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingHowWork"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Anonymous GitHub either downloads the complete repository and anonymizes the content, or proxies Anonymous GitHub either downloads the complete repository and anonymizes the content, or proxies
@@ -101,25 +70,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingScope"> <div class="panel-heading p-3" role="tab" id="headingScope">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#scope" aria-expanded="false" aria-controls="scope">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-general"
href="#scope"
aria-expanded="false"
aria-controls="scope"
>
What is the scope of anonymization? What is the scope of anonymization?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="scope" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingScope">
id="scope"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingScope"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
In double-anonymous peer review, the boundary of anonymization is the paper plus its online In double-anonymous peer review, the boundary of anonymization is the paper plus its online
@@ -135,25 +91,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingCost"> <div class="panel-heading p-3" role="tab" id="headingCost">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-general" href="#cost" aria-expanded="false" aria-controls="cost">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-general"
href="#cost"
aria-expanded="false"
aria-controls="cost"
>
How much does it cost? How much does it cost?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="cost" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingCost">
id="cost"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingCost"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Anonymous GitHub is completely free to use. However, the server costs hundreds of dollars per year Anonymous GitHub is completely free to use. However, the server costs hundreds of dollars per year
@@ -172,34 +115,16 @@
<h4 class="faq-cat"> <h4 class="faq-cat">
<i class="fas fa-cogs mr-2"></i>Features <i class="fas fa-cogs mr-2"></i>Features
</h4> </h4>
<div <div class="panel-group" id="faq-features" role="tablist" aria-multiselectable="true">
class="panel-group"
id="faq-features"
role="tablist"
aria-multiselectable="true"
>
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingFormats"> <div class="panel-heading p-3" role="tab" id="headingFormats">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#formats" aria-expanded="false" aria-controls="formats">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-features"
href="#formats"
aria-expanded="false"
aria-controls="formats"
>
Which file formats are supported? Which file formats are supported?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="formats" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingFormats">
id="formats"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingFormats"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Anonymous GitHub can display and render a wide variety of file types: Anonymous GitHub can display and render a wide variety of file types:
@@ -222,25 +147,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingPR"> <div class="panel-heading p-3" role="tab" id="headingPR">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#pullRequests" aria-expanded="false" aria-controls="pullRequests">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-features"
href="#pullRequests"
aria-expanded="false"
aria-controls="pullRequests"
>
Can I anonymize pull requests? Can I anonymize pull requests?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="pullRequests" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingPR">
id="pullRequests"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingPR"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Yes. In addition to full repositories, Anonymous GitHub supports anonymizing individual pull Yes. In addition to full repositories, Anonymous GitHub supports anonymizing individual pull
@@ -254,25 +166,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingConference"> <div class="panel-heading p-3" role="tab" id="headingConference">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#conference" aria-expanded="false" aria-controls="conference">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-features"
href="#conference"
aria-expanded="false"
aria-controls="conference"
>
What is the Conference ID feature? What is the Conference ID feature?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="conference" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingConference">
id="conference"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingConference"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
When creating an anonymized repository, you can associate it with a Conference ID. This allows When creating an anonymized repository, you can associate it with a Conference ID. This allows
@@ -287,25 +186,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingExpiration"> <div class="panel-heading p-3" role="tab" id="headingExpiration">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#expiration" aria-expanded="false" aria-controls="expiration">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-features"
href="#expiration"
aria-expanded="false"
aria-controls="expiration"
>
What happens when an anonymized repository expires? What happens when an anonymized repository expires?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="expiration" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingExpiration">
id="expiration"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingExpiration"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
You can configure one of three expiration strategies when creating your anonymized repository: You can configure one of three expiration strategies when creating your anonymized repository:
@@ -322,25 +208,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingDownload"> <div class="panel-heading p-3" role="tab" id="headingDownload">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#download" aria-expanded="false" aria-controls="download">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-features"
href="#download"
aria-expanded="false"
aria-controls="download"
>
Can I download an anonymized repository? Can I download an anonymized repository?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="download" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingDownload">
id="download"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingDownload"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Yes. Anonymized repositories can be downloaded as a ZIP file. This is useful if reviewers Yes. Anonymized repositories can be downloaded as a ZIP file. This is useful if reviewers
@@ -354,25 +227,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingUpdates"> <div class="panel-heading p-3" role="tab" id="headingUpdates">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#updates" aria-expanded="false" aria-controls="updates">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-features"
href="#updates"
aria-expanded="false"
aria-controls="updates"
>
Are updates to the original repository reflected in the anonymized version? Are updates to the original repository reflected in the anonymized version?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="updates" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingUpdates">
id="updates"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingUpdates"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Yes. The anonymized repository tracks the source on GitHub: when you push new commits to the Yes. The anonymized repository tracks the source on GitHub: when you push new commits to the
@@ -405,25 +265,12 @@
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingCLI"> <div class="panel-heading p-3" role="tab" id="headingCLI">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-features" href="#cli" aria-expanded="false" aria-controls="cli">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-features"
href="#cli"
aria-expanded="false"
aria-controls="cli"
>
Is there a command-line tool? Is there a command-line tool?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="cli" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingCLI">
id="cli"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingCLI"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Yes. Anonymous GitHub provides a CLI tool that lets you anonymize repositories locally, Yes. Anonymous GitHub provides a CLI tool that lets you anonymize repositories locally,
@@ -440,34 +287,16 @@ anonymous_github</code></pre>
<h4 class="faq-cat"> <h4 class="faq-cat">
<i class="fas fa-exclamation-triangle mr-2"></i>Limitations <i class="fas fa-exclamation-triangle mr-2"></i>Limitations
</h4> </h4>
<div <div class="panel-group" id="faq-limitations" role="tablist" aria-multiselectable="true">
class="panel-group"
id="faq-limitations"
role="tablist"
aria-multiselectable="true"
>
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingLimitation"> <div class="panel-heading p-3" role="tab" id="headingLimitation">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-limitations" href="#limitation" aria-expanded="false" aria-controls="limitation">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-limitations"
href="#limitation"
aria-expanded="false"
aria-controls="limitation"
>
What are the limitations of Anonymous GitHub? What are the limitations of Anonymous GitHub?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="limitation" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingLimitation">
id="limitation"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingLimitation"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<ul> <ul>
<li> <li>
@@ -495,34 +324,16 @@ anonymous_github</code></pre>
<h4 class="faq-cat"> <h4 class="faq-cat">
<i class="fas fa-shield-alt mr-2"></i>Privacy &amp; Security <i class="fas fa-shield-alt mr-2"></i>Privacy &amp; Security
</h4> </h4>
<div <div class="panel-group" id="faq-privacy" role="tablist" aria-multiselectable="true">
class="panel-group"
id="faq-privacy"
role="tablist"
aria-multiselectable="true"
>
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingData"> <div class="panel-heading p-3" role="tab" id="headingData">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-privacy" href="#data" aria-expanded="false" aria-controls="data">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-privacy"
href="#data"
aria-expanded="false"
aria-controls="data"
>
How is my data handled? How is my data handled?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="data" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingData">
id="data"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingData"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the
@@ -537,25 +348,12 @@ anonymous_github</code></pre>
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingPermissions"> <div class="panel-heading p-3" role="tab" id="headingPermissions">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-privacy" href="#permissions" aria-expanded="false" aria-controls="permissions">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-privacy"
href="#permissions"
aria-expanded="false"
aria-controls="permissions"
>
Why does GitHub say Anonymous GitHub asks for write access? Why does GitHub say Anonymous GitHub asks for write access?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="permissions" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingPermissions">
id="permissions"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingPermissions"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Anonymous GitHub only reads your repositories &mdash; it Anonymous GitHub only reads your repositories &mdash; it
@@ -586,25 +384,12 @@ anonymous_github</code></pre>
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingViewer"> <div class="panel-heading p-3" role="tab" id="headingViewer">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-privacy" href="#viewer" aria-expanded="false" aria-controls="viewer">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-privacy"
href="#viewer"
aria-expanded="false"
aria-controls="viewer"
>
Can repository owners see who viewed their anonymized repository? Can repository owners see who viewed their anonymized repository?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="viewer" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingViewer">
id="viewer"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingViewer"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
No. Only the total number of views is tracked as an incremental counter. It is not possible No. Only the total number of views is tracked as an incremental counter. It is not possible
@@ -620,34 +405,16 @@ anonymous_github</code></pre>
<h4 class="faq-cat"> <h4 class="faq-cat">
<i class="fas fa-server mr-2"></i>Self-Hosting <i class="fas fa-server mr-2"></i>Self-Hosting
</h4> </h4>
<div <div class="panel-group" id="faq-hosting" role="tablist" aria-multiselectable="true">
class="panel-group"
id="faq-hosting"
role="tablist"
aria-multiselectable="true"
>
<div class="panel panel-default mb-3"> <div class="panel panel-default mb-3">
<div class="panel-heading p-3" role="tab" id="headingDeploy"> <div class="panel-heading p-3" role="tab" id="headingDeploy">
<h3 class="panel-title"> <h3 class="panel-title">
<a <a class="collapsed" role="button" data-toggle="collapse" data-parent="#faq-hosting" href="#deploy" aria-expanded="false" aria-controls="deploy">
class="collapsed"
role="button"
data-toggle="collapse"
data-parent="#faq-hosting"
href="#deploy"
aria-expanded="false"
aria-controls="deploy"
>
Can I deploy my own instance? Can I deploy my own instance?
</a> </a>
</h3> </h3>
</div> </div>
<div <div id="deploy" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingDeploy">
id="deploy"
class="panel-collapse collapse"
role="tabpanel"
aria-labelledby="headingDeploy"
>
<div class="panel-body p-3"> <div class="panel-body p-3">
<p> <p>
Yes. Anonymous GitHub is fully open source (GPL-3.0) and supports Docker-based deployment. Yes. Anonymous GitHub is fully open source (GPL-3.0) and supports Docker-based deployment.
@@ -683,4 +450,3 @@ anonymous_github</code></pre>
</section> </section>
</div> </div>
</div> </div>
</div>
+31 -57
View File
@@ -1,4 +1,4 @@
<div class="pr-page" ng-init="tabState = { active: (details && details.files) ? 'files' : 'comments' }"> <div class="pr-page">
<div class="container paper-page pr-page-inner"> <div class="container paper-page pr-page-inner">
<div class="paper-crumbs"> <div class="paper-crumbs">
<a href="/dashboard">Reviewer</a> &nbsp;/&nbsp; <a href="/dashboard">Reviewer</a> &nbsp;/&nbsp;
@@ -7,94 +7,68 @@
<header class="pr-header"> <header class="pr-header">
<h1 class="paper-page-title pr-title"> <h1 class="paper-page-title pr-title">
<span ng-if="details.description" ng-bind="details.description"></span> <span v-if="details?.description" v-text="details?.description"></span>
<span ng-if="!details.description" class="text-muted">Untitled gist</span> <span class="text-muted" v-if="!details?.description">Untitled gist</span>
<a <a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl(&quot;/gist-anonymize/&quot; + (gistId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
ng-if="options.isAdmin || options.isOwner"
ng-href="/gist-anonymize/{{gistId}}"
class="btn btn-sm"
aria-label="Edit"
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
>
</h1> </h1>
<div class="pr-header-meta"> <div class="pr-header-meta">
<span class="paper-pill" ng-class="{'good': details.isPublic, 'warn': !details.isPublic}"> <span class="paper-pill" :class="{&#x27;good&#x27;: details?.isPublic, &#x27;warn&#x27;: !details?.isPublic}">
{{ details.isPublic ? 'Public' : 'Secret' }} {{ details?.isPublic ? 'Public' : 'Secret' }}
</span> </span>
<span class="pr-meta-item" ng-if="details.ownerLogin"> <span class="pr-meta-item" v-if="details?.ownerLogin">
<i class="far fa-user"></i> @<span ng-bind="details.ownerLogin"></span> <i class="far fa-user"></i> @<span v-text="details?.ownerLogin"></span>
</span> </span>
<span class="pr-meta-item" ng-if="details.updatedDate"> <span class="pr-meta-item" v-if="details?.updatedDate">
<i class="far fa-clock"></i> <span ng-bind="details.updatedDate | date"></span> <i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
</span> </span>
<span class="pr-meta-item" ng-if="details.anonymizeDate"> <span class="pr-meta-item" v-if="details?.anonymizeDate">
<i class="fas fa-user-secret"></i> Anonymized <span ng-bind="details.anonymizeDate | date"></span> <i class="fas fa-user-secret"></i> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
</span> </span>
</div> </div>
</header> </header>
<nav class="paper-tabs" ng-if="(details.files && details.files.length) || (details.comments && details.comments.length)" role="tablist"> <nav class="paper-tabs" role="tablist" v-if="(details?.files &amp;&amp; details?.files?.length) || (details?.comments &amp;&amp; details?.comments?.length)">
<button <button class="paper-tab" type="button" role="tab" v-if="details?.files" :class="{&#x27;active&#x27;: tabState?.active == &#x27;files&#x27;}" @click="tabState.active = &#x27;files&#x27;">
class="paper-tab"
ng-if="details.files"
ng-class="{'active': tabState.active == 'files'}"
ng-click="tabState.active = 'files'"
type="button"
role="tab"
>
<i class="fas fa-file-code"></i> <i class="fas fa-file-code"></i>
<ng-pluralize <span v-text="fmt.plural(details?.files?.length, {&#x27;0&#x27;: &#x27;No files&#x27;, &#x27;one&#x27;: &#x27;1 file&#x27;, &#x27;other&#x27;: &#x27;{} files&#x27;})"></span>
count="details.files.length"
when="{'0': 'No files', 'one': '1 file', 'other': '{} files'}"
></ng-pluralize>
</button> </button>
<button <button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{&#x27;active&#x27;: tabState?.active == &#x27;comments&#x27;}" @click="tabState.active = &#x27;comments&#x27;">
class="paper-tab"
ng-if="details.comments"
ng-class="{'active': tabState.active == 'comments'}"
ng-click="tabState.active = 'comments'"
type="button"
role="tab"
>
<i class="far fa-comment-dots"></i> <i class="far fa-comment-dots"></i>
<ng-pluralize <span v-text="fmt.plural(details?.comments?.length, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></span>
count="details.comments.length"
when="{'0': 'No comments', 'one': '1 comment', 'other': '{} comments'}"
></ng-pluralize>
</button> </button>
</nav> </nav>
<div class="paper-tab-content"> <div class="paper-tab-content">
<div ng-if="details.files && tabState.active =='files'"> <div v-if="details?.files &amp;&amp; tabState?.active ==&#x27;files&#x27;">
<ul class="pr-comments"> <ul class="pr-comments">
<li class="pr-comment" ng-repeat="file in details.files"> <li class="pr-comment" v-for="(file, index) in details?.files">
<div class="pr-comment-head"> <div class="pr-comment-head">
<strong ng-bind="file.filename"></strong> <strong v-text="file?.filename"></strong>
<span class="pr-comment-date" ng-if="file.language">{{file.language}}</span> <span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
</div> </div>
<gist-file file="file"></gist-file> <gist-file :file="file"></gist-file>
</li> </li>
<li class="paper-table-empty" ng-if="!details.files.length"> <li class="paper-table-empty" v-if="!details?.files?.length">
<i class="fas fa-file"></i> <i class="fas fa-file"></i>
<span>No files in this gist.</span> <span>No files in this gist.</span>
</li> </li>
</ul> </ul>
</div> </div>
<div ng-if="details.comments && tabState.active =='comments'"> <div v-if="details?.comments &amp;&amp; tabState?.active ==&#x27;comments&#x27;">
<ul class="pr-comments"> <ul class="pr-comments">
<li class="pr-comment" ng-repeat="comment in details.comments"> <li class="pr-comment" v-for="(comment, index) in details?.comments">
<div class="pr-comment-head"> <div class="pr-comment-head">
<span class="pr-comment-author" ng-if="comment.author"> <span class="pr-comment-author" v-if="comment?.author">
<i class="far fa-user"></i> @<span ng-bind="comment.author"></span> <i class="far fa-user"></i> @<span v-text="comment?.author"></span>
</span> </span>
<span class="pr-comment-date" ng-if="comment.updatedDate" ng-bind="comment.updatedDate | date"></span> <span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
</div> </div>
<div class="pr-comment-body" ng-if="comment.body"> <div class="pr-comment-body" v-if="comment?.body">
<markdown content="comment.body"></markdown> <markdown :content="comment?.body"></markdown>
</div> </div>
</li> </li>
<li class="paper-table-empty" ng-if="!details.comments.length"> <li class="paper-table-empty" v-if="!details?.comments?.length">
<i class="far fa-comment-dots"></i> <i class="far fa-comment-dots"></i>
<span>No comments on this gist.</span> <span>No comments on this gist.</span>
</li> </li>
+27 -91
View File
@@ -1,68 +1,39 @@
<nav <nav class="navbar navbar-expand-lg" :class="{&#x27;navbar-dark&#x27;: isDarkMode}">
class="navbar navbar-expand-lg"
ng-class="{'navbar-dark': isDarkMode}"
>
<a class="navbar-brand" href="/">Anonymous <em>GitHub</em></a> <a class="navbar-brand" href="/">Anonymous <em>GitHub</em></a>
<button <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto smooth-scroll"> <ul class="navbar-nav mr-auto smooth-scroll">
<li class="nav-item" ng-if="!user"> <li class="nav-item" v-if="!user">
<a class="nav-link" ng-class="{'active': path == '/'}" href="/"> <a class="nav-link" href="/" :class="{&#x27;active&#x27;: path == &#x27;/&#x27;}">
Home Home
</a> </a>
</li> </li>
<li class="nav-item" ng-if="user"> <li class="nav-item" v-if="user">
<a <a class="nav-link" href="/dashboard" :class="{&#x27;active&#x27;: path == &#x27;/dashboard&#x27;}">
class="nav-link"
ng-class="{'active': path == '/dashboard'}"
href="/dashboard"
>
<i class="fas fa-th-large d-lg-none mr-1"></i> <i class="fas fa-th-large d-lg-none mr-1"></i>
My work My work
</a> </a>
</li> </li>
<li class="nav-item" ng-if="user"> <li class="nav-item" v-if="user">
<a <a class="nav-link" href="/anonymize" :class="{&#x27;active&#x27;:path == &#x27;/anonymize&#x27; || path == &#x27;/pull-request-anonymize&#x27;}">
class="nav-link"
ng-class="{'active':path == '/anonymize' || path == '/pull-request-anonymize'}"
href="/anonymize"
>
<i class="fas fa-user-secret d-lg-none mr-1"></i> <i class="fas fa-user-secret d-lg-none mr-1"></i>
Anonymize Anonymize
</a> </a>
</li> </li>
<li class="nav-item" ng-if="user"> <li class="nav-item" v-if="user">
<a <a class="nav-link" href="/conferences" :class="{&#x27;active&#x27;:path == &#x27;/conferences&#x27;}">
class="nav-link"
ng-class="{'active':path == '/conferences'}"
href="/conferences"
>
<i class="fas fa-chalkboard-teacher d-lg-none mr-1"></i> <i class="fas fa-chalkboard-teacher d-lg-none mr-1"></i>
Conferences Conferences
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" ng-class="{'active':path == '/faq'}" href="/faq" <a class="nav-link" href="/faq" :class="{&#x27;active&#x27;:path == &#x27;/faq&#x27;}"><i class="fas fa-question-circle d-lg-none mr-1"></i>FAQ</a>
><i class="fas fa-question-circle d-lg-none mr-1"></i>FAQ</a
>
</li> </li>
<li class="nav-item" ng-if="user && user.isAdmin"> <li class="nav-item" v-if="user &amp;&amp; user?.isAdmin">
<a <a class="nav-link" href="/admin/" :class="{&#x27;active&#x27;:path?.indexOf(&#x27;/admin&#x27;) === 0}">
class="nav-link"
ng-class="{'active':path.indexOf('/admin') === 0}"
href="/admin/"
>
<i class="fas fa-cog d-lg-none mr-1"></i> <i class="fas fa-cog d-lg-none mr-1"></i>
Admin Admin
</a> </a>
@@ -70,71 +41,40 @@
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a <a class="nav-link nav-icon" target="_blank" href="https://github.com/tdurieux/anonymous_github/" title="Anonymous GitHub source code" aria-label="Anonymous GitHub source code on GitHub" rel="noopener" data-offset="30"><i class="fab fa-github" aria-hidden="true"></i></a>
class="nav-link nav-icon"
target="_blank"
href="https://github.com/tdurieux/anonymous_github/"
title="Anonymous GitHub source code"
aria-label="Anonymous GitHub source code on GitHub"
rel="noopener"
data-offset="30"
><i class="fab fa-github" aria-hidden="true"></i
></a>
</li> </li>
<!-- "Report a bug" is about this service. Reviewers who land on an <!-- "Report a bug" is about this service. Reviewers who land on an
anonymized repository often think it is about the paper, so the anonymized repository often think it is about the paper, so the
label names the service and the tooltip says who to contact. --> label names the service and the tooltip says who to contact. -->
<li class="nav-item d-none d-lg-block"> <li class="nav-item d-none d-lg-block">
<a <a class="nav-link" target="_blank" rel="noopener" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors." data-offset="30"><i class="fas fa-bug d-lg-none mr-1" aria-hidden="true"></i>Report a bug
class="nav-link"
target="_blank"
rel="noopener"
href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml"
title="Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors."
data-offset="30"
><i class="fas fa-bug d-lg-none mr-1" aria-hidden="true"></i>Report a bug
</a> </a>
</li> </li>
<li class="nav-item" ng-if="!isDarkMode"> <li class="nav-item" v-if="!isDarkMode">
<a class="nav-link nav-icon" href="#" ng-click="darkMode(true);" title="Switch to dark mode" aria-label="Switch to dark mode" role="button"> <a class="nav-link nav-icon" href="#" title="Switch to dark mode" aria-label="Switch to dark mode" role="button" @click.prevent="darkMode(true);">
<i class="fas fa-moon" aria-hidden="true"></i> <i class="fas fa-moon" aria-hidden="true"></i>
</a> </a>
</li> </li>
<li class="nav-item" ng-if="isDarkMode"> <li class="nav-item" v-if="isDarkMode">
<a class="nav-link nav-icon" href="#" ng-click="darkMode(false);" title="Switch to light mode" aria-label="Switch to light mode" role="button"> <a class="nav-link nav-icon" href="#" title="Switch to light mode" aria-label="Switch to light mode" role="button" @click.prevent="darkMode(false);">
<i class="fas fa-sun" aria-hidden="true"></i> <i class="fas fa-sun" aria-hidden="true"></i>
</a> </a>
</li> </li>
<li class="nav-item" ng-if="!user"> <li class="nav-item" v-if="!user">
<a class="nav-link btn-signin" target="_self" href="/github/login" data-offset="30" <a class="nav-link btn-signin" target="_self" href="/github/login" data-offset="30"><i class="fab fa-github mr-1"></i>
><i class="fab fa-github mr-1"></i>
Sign in Sign in
</a> </a>
</li> </li>
<li class="nav-item dropdown user-chip-wrap" ng-if="user"> <li class="nav-item dropdown user-chip-wrap" v-if="user">
<a <a class="nav-link user-chip dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="nav-link user-chip dropdown-toggle" <img width="22" height="22" class="rounded-circle" v-if="user?.photo" :src="safeUrl((user?.photo))">
href="#" <span class="user-chip-name">{{ user?.username }}</span>
id="navbarDropdownMenuLink"
role="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<img
ng-src="{{user.photo}}"
ng-if="user.photo"
width="22"
height="22"
class="rounded-circle"
/>
<span class="user-chip-name">{{user.username}}</span>
</a> </a>
<div class="dropdown-menu dropdown-menu-right user-menu" aria-labelledby="navbarDropdownMenuLink"> <div class="dropdown-menu dropdown-menu-right user-menu" aria-labelledby="navbarDropdownMenuLink">
<h6 class="dropdown-header">Signed in as @{{user.username}}</h6> <h6 class="dropdown-header">Signed in as @{{ user?.username }}</h6>
<a class="dropdown-item" href="/profile"> <a class="dropdown-item" href="/profile">
<i class="fas fa-sliders-h" aria-hidden="true"></i> <i class="fas fa-sliders-h" aria-hidden="true"></i>
<span>Settings<small>Defaults, quotas, account</small></span> <span>Settings<small>Defaults, quotas, account</small></span>
@@ -162,8 +102,4 @@
</ul> </ul>
</div> </div>
</nav> </nav>
<div <div class="navbar shadow generalMessage" v-if="generalMessage" v-html="sanitize(generalMessage)"></div>
class="navbar shadow generalMessage"
ng-bind-html="generalMessage"
ng-if="generalMessage"
></div>
+26 -62
View File
@@ -4,7 +4,7 @@
<div class="paper-hero-copy"> <div class="paper-hero-copy">
<div class="paper-eyebrow">For double-blind peer review</div> <div class="paper-eyebrow">For double-blind peer review</div>
<h1 class="hero-title"> <h1 class="hero-title">
Share the code,<br /> Share the code,<br>
<span class="accent">not the author.</span> <span class="accent">not the author.</span>
</h1> </h1>
<p class="hero-subtitle"> <p class="hero-subtitle">
@@ -14,18 +14,13 @@
</p> </p>
<div class="paper-cta-row"> <div class="paper-cta-row">
<a ng-if="!user" href="/github/login" target="_self" class="btn-hero"> <a href="/github/login" target="_self" class="btn-hero" v-if="!user">
<i class="fab fa-github mr-2" aria-hidden="true"></i>Sign in with GitHub <i class="fab fa-github mr-2" aria-hidden="true"></i>Sign in with GitHub
</a> </a>
<a ng-if="user" href="/anonymize" class="btn-hero"> <a href="/anonymize" class="btn-hero" v-if="user">
Anonymize a repository &rarr; Anonymize a repository &rarr;
</a> </a>
<a <a class="paper-link-arrow" target="_self" href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/">See an anonymized repository &rarr;</a>
class="paper-link-arrow"
target="_self"
href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/"
>See an anonymized repository &rarr;</a
>
</div> </div>
<ul class="paper-reassure"> <ul class="paper-reassure">
<li>Free and open source</li> <li>Free and open source</li>
@@ -39,12 +34,7 @@
<span class="dots"><i></i><i></i><i></i></span> <span class="dots"><i></i><i></i><i></i></span>
<span class="url">anonymous.4open.science/anonymize</span> <span class="url">anonymous.4open.science/anonymize</span>
</div> </div>
<img <img src="/imgs/anonymize.png" width="2880" height="1800" alt="The anonymize form with a live README preview showing the redactions">
src="/imgs/anonymize.png"
width="2880"
height="1800"
alt="The anonymize form with a live README preview showing the redactions"
/>
</div> </div>
</div> </div>
</section> </section>
@@ -68,19 +58,14 @@
</div> </div>
</div> </div>
<div class="paper-stats-grid"> <div class="paper-stats-grid">
<div class="paper-stat-card" ng-repeat="card in cards track by card.key"> <div class="paper-stat-card" v-for="(card, index) in cards" :key="card?.key">
<div class="paper-stat-value">{{card.total | bigNum}}</div> <div class="paper-stat-value">{{ fmt?.bigNum(card?.total) }}</div>
<div class="paper-stat-label">{{card.label}}</div> <div class="paper-stat-label">{{ card?.label }}</div>
<svg class="paper-stat-bars" preserveAspectRatio="none" aria-hidden="true" <svg class="paper-stat-bars" preserveAspectRatio="none" aria-hidden="true" v-if="history[card?.key].bars?.length &gt; 1" :viewBox="&quot;0 0 &quot; + (history[card?.key].viewW) + &quot; 36&quot;">
ng-attr-viewBox="0 0 {{history[card.key].viewW}} 36" <rect v-for="(b, index) in history[card?.key].bars" :key="index" :class="{&#x27;is-latest&#x27;: index === history[card.key].bars.length - 1}" :x="(b?.x)" :y="(b?.y)" :width="(b?.w)" :height="(b?.h)"></rect>
ng-if="history[card.key].bars.length > 1">
<rect ng-repeat="b in history[card.key].bars track by $index"
ng-attr-x="{{b.x}}" ng-attr-y="{{b.y}}"
ng-attr-width="{{b.w}}" ng-attr-height="{{b.h}}"
ng-class="{'is-latest': $last}"/>
</svg> </svg>
<div class="paper-stat-delta" ng-if="history[card.key].bars.length > 1"> <div class="paper-stat-delta" v-if="history[card?.key].bars?.length &gt; 1">
+{{history[card.key].deltaToday | number}} today +{{ fmt?.number(history[card?.key].deltaToday) }} today
</div> </div>
</div> </div>
</div> </div>
@@ -139,48 +124,27 @@
</section> </section>
<section class="paper-features paper-wrap" aria-label="Product features"> <section class="paper-features paper-wrap" aria-label="Product features">
<div class="paper-features-list" role="tablist" aria-label="Features" aria-orientation="vertical"> <div class="paper-features-list" role="tablist" aria-label="Features" aria-orientation="vertical">
<div <div class="paper-feature" v-for="(f, index) in features" :key="f?.key" :class="{&#x27;is-on&#x27;: feature === f?.key}">
class="paper-feature" <button type="button" class="paper-feature-tab" role="tab" @click="selectFeature(f.key)" @keydown="featureKeydown($event, index)" :id="&quot;feature-tab-&quot; + (f?.key)" :aria-controls="&quot;feature-panel-&quot; + (f?.key)" :aria-selected="(feature === f?.key)" :tabindex="(feature === f?.key ? 0 : -1)">
ng-repeat="f in features track by f.key" <span class="paper-eyebrow">{{ f?.num }} &middot; {{ f?.eyebrow }}</span>
ng-class="{'is-on': feature === f.key}" <span class="paper-feature-title">{{ f?.title }} <em>{{ f?.accent }}</em></span>
>
<button
type="button"
class="paper-feature-tab"
role="tab"
id="feature-tab-{{f.key}}"
aria-controls="feature-panel-{{f.key}}"
aria-selected="{{feature === f.key}}"
tabindex="{{feature === f.key ? 0 : -1}}"
ng-click="selectFeature(f.key)"
ng-keydown="featureKeydown($event, $index)"
>
<span class="paper-eyebrow">{{f.num}} &middot; {{f.eyebrow}}</span>
<span class="paper-feature-title">{{f.title}} <em>{{f.accent}}</em></span>
</button> </button>
<div class="paper-feature-more"> <div class="paper-feature-more">
<span class="paper-feature-title paper-feature-title-m" aria-hidden="true">{{f.title}} <em>{{f.accent}}</em></span> <span class="paper-feature-title paper-feature-title-m" aria-hidden="true">{{ f?.title }} <em>{{ f?.accent }}</em></span>
<p>{{f.text}}</p> <p>{{ f?.text }}</p>
<a class="paper-link-arrow" ng-href="{{featureHref(f)}}" ng-attr-target="{{featureTarget(f)}}">{{f.cta}} &rarr;</a> <a class="paper-link-arrow" :href="safeUrl((featureHref(f)))" :target="(featureTarget(f))">{{ f?.cta }} &rarr;</a>
</div> </div>
</div> </div>
</div> </div>
<div class="paper-features-stage"> <div class="paper-features-stage">
<div <div class="paper-feature-panel" role="tabpanel" v-for="(f, index) in features" :key="f?.key" v-show="feature === f?.key" :id="&quot;feature-panel-&quot; + (f?.key)" :aria-labelledby="&quot;feature-tab-&quot; + (f?.key)">
class="paper-feature-panel"
ng-repeat="f in features track by f.key"
ng-show="feature === f.key"
id="feature-panel-{{f.key}}"
role="tabpanel"
aria-labelledby="feature-tab-{{f.key}}"
>
<div class="paper-frame"> <div class="paper-frame">
<div class="paper-frame-bar" aria-hidden="true"> <div class="paper-frame-bar" aria-hidden="true">
<span class="dots"><i></i><i></i><i></i></span> <span class="dots"><i></i><i></i><i></i></span>
<span class="url">{{f.url}}</span> <span class="url">{{ f?.url }}</span>
</div> </div>
<div class="paper-crop paper-crop-{{f.key}}"> <div :class="&quot;paper-crop paper-crop-&quot; + (f?.key)">
<img ng-src="{{f.img}}" alt="{{f.alt}}" loading="lazy" width="2880" height="1800" /> <img loading="lazy" width="2880" height="1800" :src="safeUrl((f?.img))" :alt="(f?.alt)">
</div> </div>
</div> </div>
</div> </div>
@@ -233,8 +197,8 @@
<div class="paper-footer-col"> <div class="paper-footer-col">
<div class="paper-footer-head">Product</div> <div class="paper-footer-head">Product</div>
<a href="/anonymize">Anonymize</a> <a href="/anonymize">Anonymize</a>
<a href="/dashboard" ng-if="user">My work</a> <a href="/dashboard" v-if="user">My work</a>
<a href="/conferences" ng-if="user">Conferences</a> <a href="/conferences" v-if="user">Conferences</a>
<a href="/faq">FAQ</a> <a href="/faq">FAQ</a>
</div> </div>
<div class="paper-footer-col"> <div class="paper-footer-col">
@@ -251,7 +215,7 @@
</div> </div>
<div class="paper-footer-rule"></div> <div class="paper-footer-rule"></div>
<div class="paper-footer-foot"> <div class="paper-footer-foot">
<span>&copy; {{year || 2026}} Anonymous GitHub &middot; MIT licensed</span> <span>&copy; {{ year || 2026 }} Anonymous GitHub &middot; MIT licensed</span>
<span class="paper-footer-meta">Built for reviewers, by researchers.</span> <span class="paper-footer-meta">Built for reviewers, by researchers.</span>
</div> </div>
</footer> </footer>
+4 -4
View File
@@ -1,10 +1,10 @@
<div class="paper-empty"> <div class="paper-empty">
<div class="paper-empty-inner"> <div class="paper-empty-inner">
<div class="paper-eyebrow" ng-if="!error">Working</div> <div class="paper-eyebrow" v-if="!error">Working</div>
<div class="paper-eyebrow" ng-if="error">Error</div> <div class="paper-eyebrow" v-if="error">Error</div>
<h1 class="paper-empty-title" ng-if="!error"> <h1 class="paper-empty-title" v-if="!error">
Loading<span class="dot-anim">&hellip;</span> Loading<span class="dot-anim">&hellip;</span>
</h1> </h1>
<h1 class="paper-empty-title" ng-if="error" translate="{{error}}"></h1> <h1 class="paper-empty-title" v-text="fmt.translate((error))" v-if="error"></h1>
</div> </div>
</div> </div>
+45 -115
View File
@@ -1,13 +1,13 @@
<div class="container paper-page paper-settings"> <div class="container paper-page paper-settings">
<div class="paper-crumbs"><a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <span class="here">{{editionMode ? 'Edit' : 'New'}}</span></div> <div class="paper-crumbs"><a href="/dashboard">My work</a> &nbsp;/&nbsp; <a href="/conferences">Conferences</a> &nbsp;/&nbsp; <span class="here">{{ editionMode ? 'Edit' : 'New' }}</span></div>
<h1 class="paper-page-title">{{editionMode ? 'Edit' : 'Create a'}} <em>conference</em></h1> <h1 class="paper-page-title">{{ editionMode ? 'Edit' : 'Create a' }} <em>conference</em></h1>
<p class="paper-page-lede"> <p class="paper-page-lede">
Give chairs access to every anonymization submitted to a venue, and lift Give chairs access to every anonymization submitted to a venue, and lift
author quotas during the review window. author quotas during the review window.
</p> </p>
<div class="paper-settings-body"> <div class="paper-settings-body">
<aside class="paper-settings-toc" paper-scrollspy> <aside class="paper-settings-toc" v-paper-scrollspy>
<div class="paper-settings-toc-head">On this page</div> <div class="paper-settings-toc-head">On this page</div>
<nav> <nav>
<a href="#conf-basics">Basics</a> <a href="#conf-basics">Basics</a>
@@ -15,71 +15,44 @@
<a href="#conf-rendering">Rendering defaults</a> <a href="#conf-rendering">Rendering defaults</a>
<a href="#conf-features">Features</a> <a href="#conf-features">Features</a>
<a href="#conf-plan">Plan</a> <a href="#conf-plan">Plan</a>
<a href="#conf-billing" ng-show="plan.pricePerRepo > 0">Billing</a> <a href="#conf-billing" v-show="plan?.pricePerRepo &gt; 0">Billing</a>
</nav> </nav>
</aside> </aside>
<form class="form needs-validation paper-settings-main" name="conference" novalidate> <form class="form needs-validation paper-settings-main" name="conference" novalidate v-form="viewState">
<section id="conf-basics" class="paper-settings-section"> <section id="conf-basics" class="paper-settings-section">
<div class="paper-section-eyebrow">Basics</div> <div class="paper-section-eyebrow">Basics</div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="name">Conference name</label> <label class="paper-field-label" for="name">Conference name</label>
<input <input type="text" class="form-control" name="name" id="name" required placeholder="e.g. International Conference on Software Engineering" v-field="{ state: viewState, set: value =&gt; { options.name = value }, value: options?.name, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.name?.invalid &amp;&amp; (conference?.name?.touched || conference?.submitted)}">
type="text" <div class="invalid-feedback" v-show="conference?.name?.errors?.invalid">
class="form-control"
name="name"
id="name"
required
ng-class="{'is-invalid': conference.name.$invalid && (conference.name.$touched || conference.$submitted)}"
ng-model="options.name"
placeholder="e.g. International Conference on Software Engineering"
/>
<div class="invalid-feedback" ng-show="conference.name.$error.invalid">
The name of the conference is invalid. The name of the conference is invalid.
</div> </div>
<div class="invalid-feedback" ng-show="conference.name.$error.required"> <div class="invalid-feedback" v-show="conference?.name?.errors?.required">
The name of the conference is required. The name of the conference is required.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="url">Website</label> <label class="paper-field-label" for="url">Website</label>
<input <input type="url" class="form-control" name="url" id="url" placeholder="https://example.org" v-field="{ state: viewState, set: value =&gt; { options.url = value }, value: options?.url, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.url?.invalid &amp;&amp; (conference?.url?.touched || conference?.submitted)}">
type="url" <div class="invalid-feedback" v-show="conference?.url?.errors?.invalid">
class="form-control"
name="url"
id="url"
ng-class="{'is-invalid': conference.url.$invalid && (conference.url.$touched || conference.$submitted)}"
ng-model="options.url"
placeholder="https://example.org"
/>
<div class="invalid-feedback" ng-show="conference.url.$error.invalid">
The url of the conference is invalid. The url of the conference is invalid.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="conferenceID">Conference ID</label> <label class="paper-field-label" for="conferenceID">Conference ID</label>
<input <input type="text" class="form-control" name="conferenceID" id="conferenceID" required pattern="[a-zA-Z0-9\-_]{3,10}" placeholder="ICSE26" v-field="{ state: viewState, set: value =&gt; { options.conferenceID = value }, value: options?.conferenceID, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.conferenceID?.invalid &amp;&amp; (conference?.conferenceID?.touched || conference?.submitted)}">
type="text"
class="form-control"
name="conferenceID"
id="conferenceID"
required
pattern="[a-zA-Z0-9\-_]{3,10}"
ng-class="{'is-invalid': conference.conferenceID.$invalid && (conference.conferenceID.$touched || conference.$submitted)}"
ng-model="options.conferenceID"
placeholder="ICSE26"
/>
<small class="form-text text-muted">3&ndash;10 characters, letters, numbers, <code>-</code> and <code>_</code>. Authors reference this when they anonymize.</small> <small class="form-text text-muted">3&ndash;10 characters, letters, numbers, <code>-</code> and <code>_</code>. Authors reference this when they anonymize.</small>
<div class="invalid-feedback" ng-show="conference.conferenceID.$error.used"> <div class="invalid-feedback" v-show="conference?.conferenceID?.errors?.used">
The conference ID '{{options.conferenceID}}' is already used. The conference ID '{{ options?.conferenceID }}' is already used.
</div> </div>
<div class="invalid-feedback" ng-show="conference.conferenceID.$error.required"> <div class="invalid-feedback" v-show="conference?.conferenceID?.errors?.required">
The conference ID is required. The conference ID is required.
</div> </div>
<div class="invalid-feedback" ng-show="conference.conferenceID.$error.pattern"> <div class="invalid-feedback" v-show="conference?.conferenceID?.errors?.pattern">
The format of the conference ID is incorrect ([a-zA-Z0-9-_]{3,10}). The format of the conference ID is incorrect ([a-zA-Z0-9-_]{3,10}).
</div> </div>
</div> </div>
@@ -91,40 +64,24 @@
<div class="form-grid-2"> <div class="form-grid-2">
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="startDate">Start date</label> <label class="paper-field-label" for="startDate">Start date</label>
<input <input type="date" class="form-control" name="startDate" id="startDate" required v-field="{ state: viewState, set: value =&gt; { options.startDate = value }, value: options?.startDate, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.startDate?.invalid &amp;&amp; (conference?.startDate?.touched || conference?.submitted)}">
type="date"
class="form-control"
name="startDate"
id="startDate"
required
ng-class="{'is-invalid': conference.startDate.$invalid && (conference.startDate.$touched || conference.$submitted)}"
ng-model="options.startDate"
/>
<small class="form-text text-muted">Beginning of the review process.</small> <small class="form-text text-muted">Beginning of the review process.</small>
<div class="invalid-feedback" ng-show="conference.startDate.$error.required"> <div class="invalid-feedback" v-show="conference?.startDate?.errors?.required">
Start date is required. Start date is required.
</div> </div>
<div class="invalid-feedback" ng-show="conference.startDate.$error.invalid"> <div class="invalid-feedback" v-show="conference?.startDate?.errors?.invalid">
Start date must be before end date. Start date must be before end date.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="endDate">End date</label> <label class="paper-field-label" for="endDate">End date</label>
<input <input type="date" class="form-control" name="endDate" id="endDate" required v-field="{ state: viewState, set: value =&gt; { options.endDate = value }, value: options?.endDate, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.endDate?.invalid &amp;&amp; (conference?.endDate?.touched || conference?.submitted)}">
type="date"
class="form-control"
name="endDate"
id="endDate"
required
ng-class="{'is-invalid': conference.endDate.$invalid && (conference.endDate.$touched || conference.$submitted)}"
ng-model="options.endDate"
/>
<small class="form-text text-muted">All repositories expire on this date.</small> <small class="form-text text-muted">All repositories expire on this date.</small>
<div class="invalid-feedback" ng-show="conference.endDate.$error.required"> <div class="invalid-feedback" v-show="conference?.endDate?.errors?.required">
End date is required. End date is required.
</div> </div>
<div class="invalid-feedback" ng-show="conference.endDate.$error.invalid"> <div class="invalid-feedback" v-show="conference?.endDate?.errors?.invalid">
End date is invalid. End date is invalid.
</div> </div>
</div> </div>
@@ -135,22 +92,22 @@
<div class="paper-section-eyebrow">Rendering defaults</div> <div class="paper-section-eyebrow">Rendering defaults</div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="link" name="link" ng-model="options.options.link" /> <input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value =&gt; { options.options.link = value }, value: options?.options?.link, form: &quot;conference&quot;, options: {} }">
<label class="form-check-label" for="link">Keep links</label> <label class="form-check-label" for="link">Keep links</label>
<small class="form-text text-muted">Keep or remove all links from text files.</small> <small class="form-text text-muted">Keep or remove all links from text files.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="image" name="image" ng-model="options.options.image" /> <input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value =&gt; { options.options.image = value }, value: options?.options?.image, form: &quot;conference&quot;, options: {} }">
<label class="form-check-label" for="image">Display images</label> <label class="form-check-label" for="image">Display images</label>
<small class="form-text text-muted">Images are not anonymized.</small> <small class="form-text text-muted">Images are not anonymized.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" ng-model="options.options.pdf" /> <input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value =&gt; { options.options.pdf = value }, value: options?.options?.pdf, form: &quot;conference&quot;, options: {} }">
<label class="form-check-label" for="pdf">Display PDFs</label> <label class="form-check-label" for="pdf">Display PDFs</label>
<small class="form-text text-muted">PDFs are not anonymized.</small> <small class="form-text text-muted">PDFs are not anonymized.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" ng-model="options.options.notebook" /> <input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value =&gt; { options.options.notebook = value }, value: options?.options?.notebook, form: &quot;conference&quot;, options: {} }">
<label class="form-check-label" for="notebook">Display Notebooks</label> <label class="form-check-label" for="notebook">Display Notebooks</label>
</div> </div>
</section> </section>
@@ -159,12 +116,12 @@
<div class="paper-section-eyebrow">Features</div> <div class="paper-section-eyebrow">Features</div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="update" name="update" ng-model="options.options.update" /> <input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value =&gt; { options.options.update = value }, value: options?.options?.update, form: &quot;conference&quot;, options: {} }">
<label class="form-check-label" for="update">Auto-update from GitHub</label> <label class="form-check-label" for="update">Auto-update from GitHub</label>
<small class="form-text text-muted">Pull the latest commit automatically (hourly maximum).</small> <small class="form-text text-muted">Pull the latest commit automatically (hourly maximum).</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="page" name="page" ng-model="options.options.page" /> <input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value =&gt; { options.options.page = value }, value: options?.options?.page, form: &quot;conference&quot;, options: {} }">
<label class="form-check-label" for="page">GitHub Pages</label> <label class="form-check-label" for="page">GitHub Pages</label>
<small class="form-text text-muted">Enable anonymized GitHub pages.</small> <small class="form-text text-muted">Enable anonymized GitHub pages.</small>
</div> </div>
@@ -175,92 +132,65 @@
<p class="paper-section-copy">Billed per repository per month, prorated by the day for as long as a repository stays in the conference.</p> <p class="paper-section-copy">Billed per repository per month, prorated by the day for as long as a repository stays in the conference.</p>
<div class="paper-plan-grid"> <div class="paper-plan-grid">
<label <label class="paper-plan-card" v-for="(plan, index) in plans" :class="{&#x27;selected&#x27;: options?.plan?.planID == plan?.id}">
class="paper-plan-card" <input type="radio" name="planPicker" v-field="{ state: viewState, set: value =&gt; { options.plan.planID = value }, value: options?.plan?.planID, form: &quot;conference&quot;, options: {} }" :value="plan?.id">
ng-class="{'selected': options.plan.planID == plan.id}" <div class="paper-plan-head" v-text="plan?.name"></div>
ng-repeat="plan in plans"
>
<input
type="radio"
name="planPicker"
ng-value="plan.id"
ng-model="options.plan.planID"
/>
<div class="paper-plan-head" ng-bind="plan.name"></div>
<div class="paper-plan-price"> <div class="paper-plan-price">
{{plan.pricePerRepo | number}}&euro; {{ fmt?.number(plan?.pricePerRepo) }}&euro;
<span class="paper-plan-per">/ repo / month</span> <span class="paper-plan-per">/ repo / month</span>
</div> </div>
<div class="paper-plan-desc" ng-bind-html="plan.description"></div> <div class="paper-plan-desc" v-html="sanitize(plan?.description)"></div>
</label> </label>
</div> </div>
</section> </section>
<section id="conf-billing" class="paper-settings-section" ng-if="plan.pricePerRepo > 0"> <section id="conf-billing" class="paper-settings-section" v-if="plan?.pricePerRepo &gt; 0">
<div class="paper-section-eyebrow">Billing</div> <div class="paper-section-eyebrow">Billing</div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="billing_name">Name</label> <label class="paper-field-label" for="billing_name">Name</label>
<input type="text" class="form-control" name="billing_name" id="billing_name" required <input type="text" class="form-control" name="billing_name" id="billing_name" required placeholder="First &amp; last name" v-field="{ state: viewState, set: value =&gt; { options.billing.name = value }, value: options?.billing?.name, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.billing_name?.invalid &amp;&amp; (conference?.billing_name?.touched || conference?.submitted)}">
ng-class="{'is-invalid': conference.billing_name.$invalid && (conference.billing_name.$touched || conference.$submitted)}"
ng-model="options.billing.name" placeholder="First &amp; last name" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="email">Email</label> <label class="paper-field-label" for="email">Email</label>
<input type="email" class="form-control" name="email" id="email" required <input type="email" class="form-control" name="email" id="email" required placeholder="you@example.org" v-field="{ state: viewState, set: value =&gt; { options.billing.email = value }, value: options?.billing?.email, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.email?.invalid &amp;&amp; (conference?.email?.touched || conference?.submitted)}">
ng-class="{'is-invalid': conference.email.$invalid && (conference.email.$touched || conference.$submitted)}"
ng-model="options.billing.email" placeholder="you@example.org" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="inputAddress">Address</label> <label class="paper-field-label" for="inputAddress">Address</label>
<input type="text" class="form-control" name="inputAddress" id="inputAddress" required <input type="text" class="form-control" name="inputAddress" id="inputAddress" required placeholder="1234 Main St" v-field="{ state: viewState, set: value =&gt; { options.billing.address = value }, value: options?.billing?.address, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.inputAddress?.invalid &amp;&amp; (conference?.inputAddress?.touched || conference?.submitted)}">
placeholder="1234 Main St"
ng-model="options.billing.address"
ng-class="{'is-invalid': conference.inputAddress.$invalid && (conference.inputAddress.$touched || conference.$submitted)}" />
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="inputAddress2" id="inputAddress2" <input type="text" class="form-control" name="inputAddress2" id="inputAddress2" placeholder="Apartment, studio, or floor" v-field="{ state: viewState, set: value =&gt; { options.billing.address2 = value }, value: options?.billing?.address2, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.inputAddress2?.invalid &amp;&amp; (conference?.inputAddress2?.touched || conference?.submitted)}">
ng-model="options.billing.address2"
ng-class="{'is-invalid': conference.inputAddress2.$invalid && (conference.inputAddress2.$touched || conference.$submitted)}"
placeholder="Apartment, studio, or floor" />
</div> </div>
<div class="form-grid-3"> <div class="form-grid-3">
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="city">City</label> <label class="paper-field-label" for="city">City</label>
<input type="text" class="form-control" name="city" id="city" <input type="text" class="form-control" name="city" id="city" required placeholder="City" v-field="{ state: viewState, set: value =&gt; { options.billing.city = value }, value: options?.billing?.city, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.city?.invalid &amp;&amp; (conference?.city?.touched || conference?.submitted)}">
ng-model="options.billing.city" required
ng-class="{'is-invalid': conference.city.$invalid && (conference.city.$touched || conference.$submitted)}" placeholder="City" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="country">Country</label> <label class="paper-field-label" for="country">Country</label>
<input type="text" class="form-control" name="country" id="country" <input type="text" class="form-control" name="country" id="country" required placeholder="Country" v-field="{ state: viewState, set: value =&gt; { options.billing.country = value }, value: options?.billing?.country, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.country?.invalid &amp;&amp; (conference?.country?.touched || conference?.submitted)}">
ng-model="options.billing.country" required
ng-class="{'is-invalid': conference.country.$invalid && (conference.country.$touched || conference.$submitted)}" placeholder="Country" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="zip">Zip</label> <label class="paper-field-label" for="zip">Zip</label>
<input type="text" class="form-control" name="zip" id="zip" <input type="text" class="form-control" name="zip" id="zip" required placeholder="Zip" v-field="{ state: viewState, set: value =&gt; { options.billing.zip = value }, value: options?.billing?.zip, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.zip?.invalid &amp;&amp; (conference?.zip?.touched || conference?.submitted)}">
ng-model="options.billing.zip" required
ng-class="{'is-invalid': conference.zip.$invalid && (conference.zip.$touched || conference.$submitted)}" placeholder="Zip" />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="vat">VAT number</label> <label class="paper-field-label" for="vat">VAT number</label>
<input type="text" class="form-control" name="vat" id="vat" <input type="text" class="form-control" name="vat" id="vat" placeholder="VAT Number" v-field="{ state: viewState, set: value =&gt; { options.billing.vat = value }, value: options?.billing?.vat, form: &quot;conference&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: conference?.vat?.invalid &amp;&amp; (conference?.vat?.touched || conference?.submitted)}">
ng-class="{'is-invalid': conference.vat.$invalid && (conference.vat.$touched || conference.$submitted)}"
ng-model="options.billing.vat" placeholder="VAT Number" />
</div> </div>
</section> </section>
<div class="paper-settings-footer"> <div class="paper-settings-footer">
<div class="alert alert-danger" role="alert" ng-if="error" ng-bind="error"></div> <div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
<div class="alert alert-success" role="alert" ng-if="message" ng-bind="message"></div> <div class="alert alert-success" role="alert" v-if="message" v-text="message"></div>
<button id="send" type="submit" class="btn btn-ink" ng-click="submit($event)"> <button id="send" type="submit" class="btn btn-ink" @click.prevent="submitForm($event, () =&gt; { submit($event) })">
{{editionMode ? "Update" : "Create"}} conference {{ editionMode ? "Update" : "Create" }} conference
</button> </button>
</div> </div>
</form> </form>
+16 -25
View File
@@ -1,22 +1,13 @@
<div ng-if="type == 'text'" ng-model="content" ui-ace="aceOption"></div> <div v-code-editor="{ content: content, options: aceOption }" v-if="type == &#x27;text&#x27;"></div>
<div ng-if="type == 'html'" ng-bind-html="content" class="file-content markdown-body"></div> <div class="file-content markdown-body" v-if="type == &#x27;html&#x27;" v-html="sanitize(content)"></div>
<html-doc <html-doc v-if="type == &#x27;html-doc&#x27; &amp;&amp; !showSource &amp;&amp; content != null" :content="content" :base-url="(fileBaseUrl)" :allow-scripts="allowScripts"></html-doc>
ng-if="type == 'html-doc' && !showSource && content != null" <div v-code-editor="{ content: content, options: aceOption }" v-if="(type == &#x27;code&#x27; || (type == &#x27;html-doc&#x27; &amp;&amp; showSource)) &amp;&amp; content != null"></div>
content="content" <img class="image-content" v-if="type == &#x27;image&#x27;" :src="safeUrl((url))">
base-url="{{fileBaseUrl}}" <iframe class="h-100 overflow-auto w-100 b-0" v-if="type == &#x27;media&#x27;" :src="safeUrl((url))"></iframe>
allow-scripts="allowScripts" <pdfviewer v-if="type == &#x27;pdf&#x27;" :src="safeUrl((url))"></pdfviewer>
></html-doc> <div v-if="type == &#x27;audio&#x27;"><audio controls="controls"><source :src="safeUrl((url))"></audio></div>
<div <div v-if="type == &#x27;IPython&#x27;"><notebook :file="url" :content="content"></notebook></div>
ng-if="(type == 'code' || (type == 'html-doc' && showSource)) && content != null" <div class="file-error container d-flex h-100" v-if="type == &#x27;rate_limited&#x27;">
ui-ace="aceOption"
ng-model="content"
></div>
<img ng-if="type == 'image'" class="image-content" ng-src="{{url}}"></img>
<iframe class="h-100 overflow-auto w-100 b-0" ng-if="type == 'media'" ng-src="{{url}}"></iframe>
<pdfviewer ng-if="type == 'pdf'" src="{{url}}"></pdfviewer>
<div ng-if="type == 'audio'"><audio controls="controls"><source ng-src="{{url}}" /></audio></div>
<div ng-if="type == 'IPython'"><notebook file="url"></notebook></div>
<div ng-if="type == 'rate_limited'" class="file-error container d-flex h-100">
<div class="paper-ratelimit-card m-auto" style="max-width:520px;"> <div class="paper-ratelimit-card m-auto" style="max-width:520px;">
<div class="paper-ratelimit-head"> <div class="paper-ratelimit-head">
<i class="fas fa-hourglass-half"></i> <i class="fas fa-hourglass-half"></i>
@@ -25,11 +16,11 @@
<div class="paper-error-title">GitHub API rate limit reached</div> <div class="paper-error-title">GitHub API rate limit reached</div>
</div> </div>
</div> </div>
<p class="paper-error-msg">This repository will be available in <strong>{{rateLimitCountdown}}</strong>. The page will reload automatically.</p> <p class="paper-error-msg">This repository will be available in <strong>{{ rateLimitCountdown }}</strong>. The page will reload automatically.</p>
</div> </div>
</div> </div>
<div ng-if="type == 'error'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto" translate="ERRORS.{{content}}">Error</h1></div> <div class="file-error container d-flex h-100" v-if="type == &#x27;error&#x27;"><h1 class="paper-empty-title m-auto" v-text="fmt.translate(&quot;ERRORS.&quot; + (content))">Error</h1></div>
<div ng-if="type == 'loading' && !error" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Loading&hellip;</h1></div> <div class="file-error container d-flex h-100" v-if="type == &#x27;loading&#x27; &amp;&amp; !error"><h1 class="paper-empty-title m-auto">Loading&hellip;</h1></div>
<div ng-if="type == 'empty'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Empty <em>repository</em>.</h1></div> <div class="file-error container d-flex h-100" v-if="type == &#x27;empty&#x27;"><h1 class="paper-empty-title m-auto">Empty <em>repository</em>.</h1></div>
<div ng-if="content == null && type != 'empty'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Empty <em>file</em>.</h1></div> <div class="file-error container d-flex h-100" v-if="content == null &amp;&amp; type != &#x27;empty&#x27;"><h1 class="paper-empty-title m-auto">Empty <em>file</em>.</h1></div>
<div ng-if="type == 'binary'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Unsupported <em>binary file</em>. You can <a target="_blank" ng-href="{{url}}&download=true">download it</a>.</h1></div> <div class="file-error container d-flex h-100" v-if="type == &#x27;binary&#x27;"><h1 class="paper-empty-title m-auto">Unsupported <em>binary file</em>. You can <a target="_blank" :href="safeUrl((url) + &quot;&amp;download=true&quot;)">download it</a>.</h1></div>
+41 -89
View File
@@ -14,92 +14,64 @@
<a href="/pull-request-anonymize" class="btn btn-ink"> <a href="/pull-request-anonymize" class="btn btn-ink">
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> Anonymize PR <i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> Anonymize PR
</a> </a>
<a <a title="Claim the ownership of an existing anonymized repository." data-toggle="tooltip" data-placement="bottom" href="/claim" class="btn">
title="Claim the ownership of an existing anonymized repository."
data-toggle="tooltip"
data-placement="bottom"
href="/claim"
class="btn"
>
Claim Claim
</a> </a>
</div> </div>
</div> </div>
<!-- Search + filters row --> <!-- Search + filters row -->
<form class="w-100" aria-label="Pull Requests" accept-charset="UTF-8"> <form class="w-100" aria-label="Pull Requests" accept-charset="UTF-8" v-form="viewState">
<div class="d-flex flex-column flex-md-row align-items-md-center" style="gap: 8px"> <div class="d-flex flex-column flex-md-row align-items-md-center" style="gap: 8px">
<div class="flex-grow-1"> <div class="flex-grow-1">
<input <input type="search" id="search" class="form-control" aria-label="Find a pull request..." placeholder="Find a pull request..." autocomplete="off" v-field="{ state: viewState, set: value =&gt; { search = value }, value: search, form: null, options: {} }">
type="search"
id="search"
class="form-control"
aria-label="Find a pull request..."
placeholder="Find a pull request..."
autocomplete="off"
ng-model="search"
/>
</div> </div>
<div class="d-flex flex-wrap" style="gap: 6px"> <div class="d-flex flex-wrap" style="gap: 6px">
<div class="dropdown"> <div class="dropdown">
<button <button class="btn btn-sm dropdown-toggle" type="button" id="dropdownSort" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn btn-sm dropdown-toggle"
type="button"
id="dropdownSort"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Sort Sort
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownSort"> <div class="dropdown-menu" aria-labelledby="dropdownSort">
<h6 class="dropdown-header">Select order</h6> <h6 class="dropdown-header">Select order</h6>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortFullName" value="fullName" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortFullName" value="fullName" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortFullName">Pull Request</label> <label class="form-check-label" for="sortFullName">Pull Request</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortAnonymizeDate" value="-anonymizeDate" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortAnonymizeDate" value="-anonymizeDate" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortAnonymizeDate">Anonymize Date</label> <label class="form-check-label" for="sortAnonymizeDate">Anonymize Date</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortStatus" value="-status" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortStatus">Status</label> <label class="form-check-label" for="sortStatus">Status</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortLastView" value="-lastView" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortLastView" value="-lastView" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortLastView">Last View</label> <label class="form-check-label" for="sortLastView">Last View</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="radio" name="sort" id="sortPageView" value="-pageView" ng-model="orderBy" /> <input class="form-check-input" type="radio" name="sort" id="sortPageView" value="-pageView" v-field="{ state: viewState, set: value =&gt; { orderBy = value }, value: orderBy, form: null, options: {} }">
<label class="form-check-label" for="sortPageView">Page View</label> <label class="form-check-label" for="sortPageView">Page View</label>
</div> </div>
</div> </div>
</div> </div>
<div class="dropdown"> <div class="dropdown">
<button <button class="btn btn-sm dropdown-toggle" type="button" id="dropdownStatus" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn btn-sm dropdown-toggle"
type="button"
id="dropdownStatus"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Status Status
</button> </button>
<div class="dropdown-menu" aria-labelledby="dropdownStatus"> <div class="dropdown-menu" aria-labelledby="dropdownStatus">
<h6 class="dropdown-header">Select status</h6> <h6 class="dropdown-header">Select status</h6>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="checkbox" name="sort" id="statusReady" value="ready" ng-model="filters.status.ready" /> <input class="form-check-input" type="checkbox" name="sort" id="statusReady" value="ready" v-field="{ state: viewState, set: value =&gt; { filters.status.ready = value }, value: filters?.status?.ready, form: null, options: {} }">
<label class="form-check-label" for="statusReady">Ready</label> <label class="form-check-label" for="statusReady">Ready</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="checkbox" name="sort" id="statusExpired" value="expired" ng-model="filters.status.expired" /> <input class="form-check-input" type="checkbox" name="sort" id="statusExpired" value="expired" v-field="{ state: viewState, set: value =&gt; { filters.status.expired = value }, value: filters?.status?.expired, form: null, options: {} }">
<label class="form-check-label" for="statusExpired">Expired</label> <label class="form-check-label" for="statusExpired">Expired</label>
</div> </div>
<div class="form-check dropdown-item"> <div class="form-check dropdown-item">
<input class="form-check-input" type="checkbox" name="sort" id="statusRemoved" value="removed" ng-model="filters.status.removed" /> <input class="form-check-input" type="checkbox" name="sort" id="statusRemoved" value="removed" v-field="{ state: viewState, set: value =&gt; { filters.status.removed = value }, value: filters?.status?.removed, form: null, options: {} }">
<label class="form-check-label" for="statusRemoved">Removed</label> <label class="form-check-label" for="statusRemoved">Removed</label>
</div> </div>
</div> </div>
@@ -109,10 +81,10 @@
</form> </form>
<!-- Active filter chips --> <!-- Active filter chips -->
<div class="d-flex flex-wrap mt-2" style="gap: 6px" ng-show="filters.status.ready === false || filters.status.expired === false || filters.status.removed === false"> <div class="d-flex flex-wrap mt-2" style="gap: 6px" v-show="filters?.status?.ready === false || filters?.status?.expired === false || filters?.status?.removed === false">
<span class="filter-chip" ng-show="!v" ng-repeat="(f, v) in filters.status"> <span class="filter-chip" v-for="(v, f, index) in filters?.status" v-show="!v">
{{f | title}} {{ fmt?.title(f) }}
<button type="button" class="filter-chip-close" aria-label="Remove filter" ng-click="filters.status[f] = true;"> <button type="button" class="filter-chip-close" aria-label="Remove filter" @click="filters.status[f] = true;">
&times; &times;
</button> </button>
</span> </span>
@@ -121,88 +93,68 @@
<!-- Pull request list --> <!-- Pull request list -->
<ul class="repo-list w-100"> <ul class="repo-list w-100">
<li <li class="repo-list-item" v-for="(pr, index) in filteredPullRequests" :class="{&#x27;repo-inactive&#x27;: pr?.status == &#x27;expired&#x27; || pr?.status == &#x27;removed&#x27; || pr?.status == &#x27;error&#x27;}">
class="repo-list-item"
ng-class="{'repo-inactive': pr.status == 'expired' || pr.status == 'removed' || pr.status == 'error'}"
ng-repeat="pr in pullRequests| filter:pullRequestFilter| orderBy:orderBy as filteredPullRequests"
>
<div class="repo-list-item-content"> <div class="repo-list-item-content">
<div class="repo-list-item-main"> <div class="repo-list-item-main">
<div class="repo-list-item-header"> <div class="repo-list-item-header">
<a ng-href="/pr/{{pr.pullRequestId}}" class="repo-name" ng-bind="pr.pullRequestId"></a> <a class="repo-name" v-text="pr?.pullRequestId" :href="safeUrl(&quot;/pr/&quot; + (pr?.pullRequestId))"></a>
<span <span class="status-badge" :class="{&#x27;status-removed&#x27;: pr?.status == &#x27;removed&#x27; || pr?.status == &#x27;expired&#x27; || pr?.status == &#x27;removing&#x27; || pr?.status == &#x27;expiring&#x27;, &#x27;status-preparing&#x27;: pr?.status == &#x27;preparing&#x27; || pr?.status == &#x27;download&#x27;, &#x27;status-ready&#x27;: pr?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: pr?.status == &#x27;error&#x27;}"><span v-text="fmt?.title(pr?.status)"></span><span v-if="pr?.status == &#x27;error&#x27;" v-text="&#x27;: &#x27; + pr?.statusMessage"></span></span>
class="status-badge"
ng-class="{'status-removed': pr.status == 'removed' || pr.status == 'expired' || pr.status == 'removing' || pr.status == 'expiring', 'status-preparing': pr.status == 'preparing' || pr.status == 'download', 'status-ready': pr.status == 'ready', 'status-error': pr.status == 'error'}"
><span ng-bind="pr.status | title"></span><span
ng-if="pr.status == 'error'"
ng-bind="': ' + pr.statusMessage"
></span></span>
</div> </div>
<div class="repo-source"> <div class="repo-source">
<span> <span>
<i class="fab fa-github" aria-hidden="true"></i> <i class="fab fa-github" aria-hidden="true"></i>
<a href="https://github.com/{{pr.source.repositoryFullName}}/pull/{{pr.source.pullRequestId}}">{{pr.source.repositoryFullName}}#{{pr.source.pullRequestId}}</a> <a :href="safeUrl(&quot;https://github.com/&quot; + (pr?.source?.repositoryFullName) + &quot;/pull/&quot; + (pr?.source?.pullRequestId))">{{ pr?.source?.repositoryFullName }}#{{ pr?.source?.pullRequestId }}</a>
</span> </span>
<span <span class="status-badge" style="font-size: 10px;" :class="{&#x27;status-ready&#x27;: pr?.merged, &#x27;status-removed&#x27;: pr?.state==&#x27;open&#x27;, &#x27;status-error&#x27;: pr?.state==&#x27;closed&#x27; &amp;&amp; !pr?.merged}">
class="status-badge" {{ fmt?.title(pr?.merged ? "merged" : pr?.state) }}
ng-class="{'status-ready': pr.merged, 'status-removed': pr.state=='open', 'status-error': pr.state=='closed' && !pr.merged}"
style="font-size: 10px;"
>
{{pr.merged ? "merged" : pr.state | title}}
</span> </span>
<span class="repo-date">anonymized {{pr.anonymizeDate | humanTime}}</span> <span class="repo-date">anonymized {{ fmt?.humanTime(pr?.anonymizeDate) }}</span>
</div> </div>
</div> </div>
<div class="repo-meta"> <div class="repo-meta">
<span ng-if="::pr.conference" title="Conference"> <span title="Conference" v-if="pr?.conference">
<i class="fas fa-chalkboard-teacher"></i> {{pr.conference}} <i class="fas fa-chalkboard-teacher"></i> {{ pr?.conference }}
</span> </span>
<span title="Terms: {{::pr.options.terms.join(', ')}}" data-toggle="tooltip" data-placement="bottom"> <span data-toggle="tooltip" data-placement="bottom" :title="&quot;Terms: &quot; + (pr?.options?.terms?.join(&#x27;, &#x27;))">
<i class="fas fa-shield-alt"></i> {{::pr.options.terms.length | number}} <i class="fas fa-shield-alt"></i> {{ fmt?.number(pr?.options?.terms?.length) }}
</span> </span>
<span title="Views: {{::pr.pageView | number}}" data-toggle="tooltip" data-placement="bottom"> <span data-toggle="tooltip" data-placement="bottom" :title="&quot;Views: &quot; + (fmt?.number(pr?.pageView))">
<i class="far fa-eye"></i> {{::pr.pageView | number}} <i class="far fa-eye"></i> {{ fmt?.number(pr?.pageView) }}
</span> </span>
<span title="Last view: {{::pr.lastView | date}}" data-toggle="tooltip" data-placement="bottom"> <span data-toggle="tooltip" data-placement="bottom" :title="&quot;Last view: &quot; + (fmt?.date(pr?.lastView))">
<i class="far fa-calendar-alt"></i> {{::pr.lastView | humanTime}} <i class="far fa-calendar-alt"></i> {{ fmt?.humanTime(pr?.lastView) }}
</span> </span>
<span ng-if="pr.options.expirationMode!='never' && pr.status == 'ready'"> <span v-if="pr?.options?.expirationMode!=&#x27;never&#x27; &amp;&amp; pr?.status == &#x27;ready&#x27;">
<i class="far fa-clock"></i> Expire: {{pr.options.expirationDate | humanTime}} <i class="far fa-clock"></i> Expire: {{ fmt?.humanTime(pr?.options?.expirationDate) }}
</span> </span>
</div> </div>
</div> </div>
<div class="repo-list-item-actions"> <div class="repo-list-item-actions">
<div class="dropdown"> <div class="dropdown">
<button <button class="btn btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
class="btn btn-sm dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Actions Actions
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="/pull-request-anonymize/{{pr.pullRequestId}}"> <a class="dropdown-item" :href="safeUrl(&quot;/pull-request-anonymize/&quot; + (pr?.pullRequestId))">
<i class="far fa-edit" aria-hidden="true"></i> Edit <i class="far fa-edit" aria-hidden="true"></i> Edit
</a> </a>
<a class="dropdown-item" href="#" ng-show="pr.status == 'ready' || pr.status == 'error'" ng-click="updatePullRequest(pr)"> <a class="dropdown-item" href="#" v-show="pr?.status == &#x27;ready&#x27; || pr?.status == &#x27;error&#x27;" @click.prevent="updatePullRequest(pr)">
<i class="fas fa-sync"></i> Force update <i class="fas fa-sync"></i> Force update
</a> </a>
<a class="dropdown-item" href="#" ng-show="pr.status == 'removed'" ng-click="updatePullRequest(pr)"> <a class="dropdown-item" href="#" v-show="pr?.status == &#x27;removed&#x27;" @click.prevent="updatePullRequest(pr)">
<i class="fas fa-check-circle"></i> Enable <i class="fas fa-check-circle"></i> Enable
</a> </a>
<a class="dropdown-item" href="#" ng-show="pr.status == 'ready'" ng-click="removePullRequest(pr)"> <a class="dropdown-item" href="#" v-show="pr?.status == &#x27;ready&#x27;" @click.prevent="removePullRequest(pr)">
<i class="fas fa-trash-alt"></i> Remove <i class="fas fa-trash-alt"></i> Remove
</a> </a>
<a class="dropdown-item" href="/pr/{{pr.pullRequestId}}/"> <a class="dropdown-item" :href="safeUrl(&quot;/pr/&quot; + (pr?.pullRequestId) + &quot;/&quot;)">
<i class="fa fa-eye" aria-hidden="true"></i> View PR <i class="fa fa-eye" aria-hidden="true"></i> View PR
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
<li class="repo-list-empty" ng-if="filteredPullRequests.length == 0"> <li class="repo-list-empty" v-if="filteredPullRequests?.length == 0">
<i class="fas fa-inbox"></i> <i class="fas fa-inbox"></i>
<span>No pull requests to display.</span> <span>No pull requests to display.</span>
</li> </li>
+40 -55
View File
@@ -4,7 +4,7 @@
<p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p> <p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>
<div class="paper-settings-body"> <div class="paper-settings-body">
<aside class="paper-settings-toc" paper-scrollspy> <aside class="paper-settings-toc" v-paper-scrollspy>
<div class="paper-settings-toc-head">On this page</div> <div class="paper-settings-toc-head">On this page</div>
<nav> <nav>
<a href="#settings-quota">Quota</a> <a href="#settings-quota">Quota</a>
@@ -21,65 +21,50 @@
<section id="settings-quota" class="paper-settings-section"> <section id="settings-quota" class="paper-settings-section">
<div class="paper-section-eyebrow">Quota</div> <div class="paper-section-eyebrow">Quota</div>
<p class="paper-section-copy">What you are using across all your anonymizations. Conferences can lift these limits during a review window.</p> <p class="paper-section-copy">What you are using across all your anonymizations. Conferences can lift these limits during a review window.</p>
<div class="quota-row" ng-if="quota"> <div class="quota-row" v-if="quota">
<div class="quota-item" ng-repeat="q in [ <div class="quota-item" v-for="(q, index) in [
{key: 'repository', label: 'Repositories', kind: 'count'}, {key: &#x27;repository&#x27;, label: &#x27;Repositories&#x27;, kind: &#x27;count&#x27;},
{key: 'storage', label: 'Storage', kind: 'bytes'}, {key: &#x27;storage&#x27;, label: &#x27;Storage&#x27;, kind: &#x27;bytes&#x27;},
{key: 'file', label: 'Files', kind: 'count'} {key: &#x27;file&#x27;, label: &#x27;Files&#x27;, kind: &#x27;count&#x27;}
]"> ]">
<div class="quota-header"> <div class="quota-header">
<span class="quota-label">{{q.label}}</span> <span class="quota-label">{{ q?.label }}</span>
<span class="quota-value" ng-if="q.kind === 'count'"> <span class="quota-value" v-if="q?.kind === &#x27;count&#x27;">
{{quota[q.key].used | number}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | number}}</span> {{ fmt?.number(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.number(quota[q?.key].total) }}</span>
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span> <span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
</span> </span>
<span class="quota-value" ng-if="q.kind === 'bytes'"> <span class="quota-value" v-if="q?.kind === &#x27;bytes&#x27;">
{{quota[q.key].used | humanFileSize}}<span ng-if="!quota[q.key].unlimited"> / {{quota[q.key].total | humanFileSize}}</span> {{ fmt?.humanFileSize(quota[q?.key].used) }}<span v-if="!quota[q?.key].unlimited"> / {{ fmt?.humanFileSize(quota[q?.key].total) }}</span>
<span class="quota-unlimited-tag" ng-if="quota[q.key].unlimited">Unlimited</span> <span class="quota-unlimited-tag" v-if="quota[q?.key].unlimited">Unlimited</span>
</span> </span>
</div> </div>
<div <div class="quota-track" role="progressbar" aria-valuemin="0" :class="&#x27;quota-&#x27; + quota[q?.key].level" :aria-label="(q?.label) + &quot; quota&quot;" :aria-valuenow="(quota[q?.key].used)" :aria-valuemax="(quota[q?.key].unlimited ? quota[q?.key].used : quota[q?.key].total)" :aria-valuetext="(quota[q?.key].unlimited ? &#x27;unlimited&#x27; : fmt.number(quota[q?.key].percent, 0) + &#x27;% used&#x27;)">
class="quota-track" <div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + &#x27;%&#x27;}"></div>
ng-class="'quota-' + quota[q.key].level"
role="progressbar"
aria-label="{{q.label}} quota"
aria-valuenow="{{quota[q.key].used}}"
aria-valuemin="0"
aria-valuemax="{{quota[q.key].unlimited ? quota[q.key].used : quota[q.key].total}}"
aria-valuetext="{{quota[q.key].unlimited ? 'unlimited' : (quota[q.key].percent | number:0) + '% used'}}"
>
<div class="quota-fill" ng-if="!quota[q.key].unlimited" ng-style="{width: quota[q.key].percent + '%'}"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="quota-row" ng-if="!quota" aria-hidden="true"> <div class="quota-row" aria-hidden="true" v-if="!quota">
<div class="quota-item" ng-repeat="i in [1, 2, 3]"> <div class="quota-item" v-for="(i, index) in [1, 2, 3]">
<div class="quota-header"><span class="skeleton skeleton-line" style="width: 40%"></span><span class="skeleton skeleton-line" style="width: 25%"></span></div> <div class="quota-header"><span class="skeleton skeleton-line" style="width: 40%"></span><span class="skeleton skeleton-line" style="width: 25%"></span></div>
<div class="quota-track"></div> <div class="quota-track"></div>
</div> </div>
</div> </div>
</section> </section>
<form class="form needs-validation" name="defaultsForm" novalidate ng-submit="saveDefault($event)"> <form class="form needs-validation" name="defaultsForm" novalidate v-form="viewState" @submit.prevent="submitForm($event, () =&gt; { saveDefault($event) })">
<section id="settings-terms" class="paper-settings-section"> <section id="settings-terms" class="paper-settings-section">
<div class="paper-section-eyebrow">Terms to redact</div> <div class="paper-section-eyebrow">Terms to redact</div>
<p class="paper-section-copy">Pre-filled in every new anonymization. You can still edit the list per anonymization.</p> <p class="paper-section-copy">Pre-filled in every new anonymization. You can still edit the list per anonymization.</p>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="terms">Default terms</label> <label class="paper-field-label" for="terms">Default terms</label>
<textarea <textarea class="form-control" id="terms" name="terms" rows="4" placeholder="Jane Smith
class="form-control" MIT CSAIL
id="terms" jane.smith@example.org" v-field="{ state: viewState, set: value =&gt; { terms = value }, value: terms, form: &quot;defaultsForm&quot;, options: { debounce: 250 } }"></textarea>
name="terms"
rows="4"
placeholder="Jane Smith&#10;MIT CSAIL&#10;jane.smith@example.org"
ng-model="terms"
ng-model-options="{ debounce: 250 }"
></textarea>
<small id="termsHelp" class="form-text text-muted"> <small id="termsHelp" class="form-text text-muted">
One term per line (regex allowed). Each match is replaced by One term per line (regex allowed). Each match is replaced by
<code>{{site_options.ANONYMIZATION_MASK || 'XXX'}}-[N]</code>, or use <code>term=&gt;replacement</code> to pick your own. <code>{{ site_options?.ANONYMIZATION_MASK || 'XXX' }}-[N]</code>, or use <code>term=&gt;replacement</code> to pick your own.
</small> </small>
<div class="invalid-feedback" ng-show="defaultsForm.terms.$error.format"> <div class="invalid-feedback" v-show="defaultsForm?.terms?.errors?.format">
Terms are in an invalid format Terms are in an invalid format
</div> </div>
</div> </div>
@@ -90,22 +75,22 @@
<p class="paper-section-copy">What reviewers can see inside an anonymized repository.</p> <p class="paper-section-copy">What reviewers can see inside an anonymized repository.</p>
<div class="form-group paper-check-list"> <div class="form-group paper-check-list">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="link" name="link" ng-model="options.link" /> <input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value =&gt; { options.link = value }, value: options?.link, form: &quot;defaultsForm&quot;, options: {} }">
<label class="form-check-label" for="link">Keep links</label> <label class="form-check-label" for="link">Keep links</label>
<small class="form-text text-muted">Unchecked, every link in text files is removed.</small> <small class="form-text text-muted">Unchecked, every link in text files is removed.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="image" name="image" ng-model="options.image" /> <input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value =&gt; { options.image = value }, value: options?.image, form: &quot;defaultsForm&quot;, options: {} }">
<label class="form-check-label" for="image">Display images</label> <label class="form-check-label" for="image">Display images</label>
<small class="form-text text-muted">Images are shown as is. They are not anonymized.</small> <small class="form-text text-muted">Images are shown as is. They are not anonymized.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" ng-model="options.pdf" /> <input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value =&gt; { options.pdf = value }, value: options?.pdf, form: &quot;defaultsForm&quot;, options: {} }">
<label class="form-check-label" for="pdf">Display PDFs</label> <label class="form-check-label" for="pdf">Display PDFs</label>
<small class="form-text text-muted">PDFs are shown as is. They are not anonymized.</small> <small class="form-text text-muted">PDFs are shown as is. They are not anonymized.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" ng-model="options.notebook" /> <input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value =&gt; { options.notebook = value }, value: options?.notebook, form: &quot;defaultsForm&quot;, options: {} }">
<label class="form-check-label" for="notebook">Display notebooks</label> <label class="form-check-label" for="notebook">Display notebooks</label>
<small class="form-text text-muted">Render Jupyter notebooks instead of raw JSON.</small> <small class="form-text text-muted">Render Jupyter notebooks instead of raw JSON.</small>
</div> </div>
@@ -116,30 +101,30 @@
<div class="paper-section-eyebrow">Features</div> <div class="paper-section-eyebrow">Features</div>
<div class="form-group paper-check-list"> <div class="form-group paper-check-list">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="page" name="page" ng-model="options.page" /> <input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value =&gt; { options.page = value }, value: options?.page, form: &quot;defaultsForm&quot;, options: {} }">
<label class="form-check-label" for="page">GitHub Pages</label> <label class="form-check-label" for="page">GitHub Pages</label>
<small class="form-text text-muted">Serve an anonymized copy of the repository's GitHub Pages site. Only pages built from the anonymized branch are supported.</small> <small class="form-text text-muted">Serve an anonymized copy of the repository's GitHub Pages site. Only pages built from the anonymized branch are supported.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="loc" name="loc" ng-model="options.loc" /> <input class="form-check-input" type="checkbox" id="loc" name="loc" v-field="{ state: viewState, set: value =&gt; { options.loc = value }, value: options?.loc, form: &quot;defaultsForm&quot;, options: {} }">
<label class="form-check-label" for="loc">Lines of code</label> <label class="form-check-label" for="loc">Lines of code</label>
<small class="form-text text-muted">Show the line count of the repository in the explorer.</small> <small class="form-text text-muted">Show the line count of the repository in the explorer.</small>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="update" name="update" ng-model="options.update" /> <input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value =&gt; { options.update = value }, value: options?.update, form: &quot;defaultsForm&quot;, options: {} }">
<label class="form-check-label" for="update">Auto-update from GitHub</label> <label class="form-check-label" for="update">Auto-update from GitHub</label>
<small class="form-text text-muted">Follow the branch and pull the latest commit automatically, at most once an hour.</small> <small class="form-text text-muted">Follow the branch and pull the latest commit automatically, at most once an hour.</small>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="mode">Fetch mode</label> <label class="paper-field-label" for="mode">Fetch mode</label>
<select class="form-control" id="mode" name="mode" ng-model="options.mode"> <select class="form-control" id="mode" name="mode" v-field="{ state: viewState, set: value =&gt; { options.mode = value }, value: options?.mode, form: &quot;defaultsForm&quot;, options: {} }">
<option value="GitHubStream" selected>Stream from GitHub on demand</option> <option value="GitHubStream" selected>Stream from GitHub on demand</option>
<option value="GitHubDownload">Download to Anonymous GitHub</option> <option value="GitHubDownload">Download to Anonymous GitHub</option>
</select> </select>
<small class="form-text text-muted"> <small class="form-text text-muted">
Download is faster for reviewers and enables every feature. Streaming is the only option above Download is faster for reviewers and enables every feature. Streaming is the only option above
{{site_options.MAX_REPO_SIZE * 1024 | humanFileSize}}. {{ fmt?.humanFileSize(site_options?.MAX_REPO_SIZE * 1024) }}.
</small> </small>
</div> </div>
</section> </section>
@@ -148,7 +133,7 @@
<div class="paper-section-eyebrow">Expiration</div> <div class="paper-section-eyebrow">Expiration</div>
<div class="form-group"> <div class="form-group">
<label class="paper-field-label" for="expiration">When an anonymization expires</label> <label class="paper-field-label" for="expiration">When an anonymization expires</label>
<select class="form-control" id="expiration" name="expiration" ng-model="options.expirationMode"> <select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;defaultsForm&quot;, options: {} }">
<option value="redirect">Redirect visitors to the GitHub repository</option> <option value="redirect">Redirect visitors to the GitHub repository</option>
<option value="remove" selected>Remove the anonymized repository</option> <option value="remove" selected>Remove the anonymized repository</option>
</select> </select>
@@ -159,10 +144,10 @@
</section> </section>
<div class="paper-settings-footer"> <div class="paper-settings-footer">
<div class="paper-inline-alert paper-inline-alert-error" role="alert" ng-if="error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span ng-bind="error"></span></div> <div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span v-text="error"></span></div>
<div class="paper-settings-actions"> <div class="paper-settings-actions">
<button id="save" type="submit" class="btn btn-ink" ng-disabled="saving"> <button id="save" type="submit" class="btn btn-ink" :disabled="saving">
<i class="fas fa-check mr-1" aria-hidden="true" ng-if="message"></i>{{ saving ? 'Saving…' : (message ? 'Saved' : 'Save defaults') }} <i class="fas fa-check mr-1" aria-hidden="true" v-if="message"></i>{{ saving ? 'Saving…' : (message ? 'Saved' : 'Save defaults') }}
</button> </button>
<span class="paper-settings-hint">Applies to new anonymizations only. Existing ones keep their settings.</span> <span class="paper-settings-hint">Applies to new anonymizations only. Existing ones keep their settings.</span>
</div> </div>
@@ -172,9 +157,9 @@
<section id="settings-account" class="paper-settings-section"> <section id="settings-account" class="paper-settings-section">
<div class="paper-section-eyebrow">Account</div> <div class="paper-section-eyebrow">Account</div>
<div class="paper-account-card"> <div class="paper-account-card">
<img ng-src="{{user.photo}}" ng-if="user.photo" width="40" height="40" class="rounded-circle" alt="" /> <img width="40" height="40" class="rounded-circle" alt="" v-if="user?.photo" :src="safeUrl((user?.photo))">
<div class="paper-account-identity"> <div class="paper-account-identity">
<div class="paper-account-name">@{{user.username}}</div> <div class="paper-account-name">@{{ user?.username }}</div>
<div class="paper-account-meta">Signed in with GitHub. Anonymous GitHub only reads your repositories.</div> <div class="paper-account-meta">Signed in with GitHub. Anonymous GitHub only reads your repositories.</div>
</div> </div>
<a class="btn btn-outline-ink" href="/api/user/logout" target="_self">Sign out</a> <a class="btn btn-outline-ink" href="/api/user/logout" target="_self">Sign out</a>
@@ -187,8 +172,8 @@
revokes the application's access to your GitHub account, and erases revokes the application's access to your GitHub account, and erases
your personal data (username, email, access tokens). This cannot be undone. your personal data (username, email, access tokens). This cannot be undone.
</p> </p>
<div class="paper-inline-alert paper-inline-alert-error" role="alert" ng-if="deleteError"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span ng-bind="deleteError"></span></div> <div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="deleteError"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <span v-text="deleteError"></span></div>
<button type="button" class="btn btn-outline-danger" ng-click="deleteAccount()" ng-disabled="deletingAccount"> <button type="button" class="btn btn-outline-danger" @click="deleteAccount()" :disabled="deletingAccount">
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>{{ deletingAccount ? 'Deleting…' : 'Delete my account' }} <i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>{{ deletingAccount ? 'Deleting…' : 'Delete my account' }}
</button> </button>
</div> </div>
+28 -51
View File
@@ -7,84 +7,61 @@
<header class="pr-header"> <header class="pr-header">
<h1 class="paper-page-title pr-title"> <h1 class="paper-page-title pr-title">
<span ng-if="details.title" ng-bind="details.title"></span> <span v-if="details?.title" v-text="details?.title"></span>
<span ng-if="!details.title" class="text-muted">Untitled pull request</span> <span class="text-muted" v-if="!details?.title">Untitled pull request</span>
<a <a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl(&quot;/pull-request-anonymize/&quot; + (pullRequestId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
ng-if="options.isAdmin || options.isOwner"
ng-href="/pull-request-anonymize/{{pullRequestId}}"
class="btn btn-sm"
aria-label="Edit"
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
>
</h1> </h1>
<div class="pr-header-meta"> <div class="pr-header-meta">
<span class="paper-pill" ng-class="{'good': details.merged, 'warn': details.state == 'open', 'bad': details.state == 'closed' && !details.merged}"> <span class="paper-pill" :class="{&#x27;good&#x27;: details?.merged, &#x27;warn&#x27;: details?.state == &#x27;open&#x27;, &#x27;bad&#x27;: details?.state == &#x27;closed&#x27; &amp;&amp; !details?.merged}">
<span class="status-dot" ng-class="{'status-ready': details.merged, 'status-error': details.state == 'closed' && !details.merged}"></span> <span class="status-dot" :class="{&#x27;status-ready&#x27;: details?.merged, &#x27;status-error&#x27;: details?.state == &#x27;closed&#x27; &amp;&amp; !details?.merged}"></span>
{{ details.merged ? 'Merged' : (details.state | title) }} {{ details?.merged ? 'Merged' : fmt.title(details?.state) }}
</span> </span>
<span class="pr-meta-item" ng-if="details.baseRepositoryFullName"> <span class="pr-meta-item" v-if="details?.baseRepositoryFullName">
<i class="fab fa-github"></i> <span ng-bind="details.baseRepositoryFullName"></span> <i class="fab fa-github"></i> <span v-text="details?.baseRepositoryFullName"></span>
</span> </span>
<span class="pr-meta-item" ng-if="details.updatedDate"> <span class="pr-meta-item" v-if="details?.updatedDate">
<i class="far fa-clock"></i> <span ng-bind="details.updatedDate | date"></span> <i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
</span> </span>
<span class="pr-meta-item" ng-if="details.anonymizeDate"> <span class="pr-meta-item" v-if="details?.anonymizeDate">
<i class="fas fa-user-secret"></i> Anonymized <span ng-bind="details.anonymizeDate | date"></span> <i class="fas fa-user-secret"></i> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
</span> </span>
</div> </div>
</header> </header>
<section class="pr-body-card" ng-if="details.body"> <section class="pr-body-card" v-if="details?.body">
<div class="paper-section-eyebrow">Description</div> <div class="paper-section-eyebrow">Description</div>
<markdown content="details.body"></markdown> <markdown :content="details?.body"></markdown>
</section> </section>
<nav class="paper-tabs" ng-if="details.diff || details.comments" role="tablist"> <nav class="paper-tabs" role="tablist" v-if="details?.diff || details?.comments">
<button <button class="paper-tab" type="button" role="tab" v-if="details?.diff" :class="{&#x27;active&#x27;: tabState?.active == &#x27;diff&#x27;}" @click="tabState.active = &#x27;diff&#x27;">
class="paper-tab"
ng-if="details.diff"
ng-class="{'active': tabState.active == 'diff'}"
ng-click="tabState.active = 'diff'"
type="button"
role="tab"
>
<i class="fas fa-code"></i> Diff <i class="fas fa-code"></i> Diff
</button> </button>
<button <button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{&#x27;active&#x27;: tabState?.active == &#x27;comments&#x27;}" @click="tabState.active = &#x27;comments&#x27;">
class="paper-tab"
ng-if="details.comments"
ng-class="{'active': tabState.active == 'comments'}"
ng-click="tabState.active = 'comments'"
type="button"
role="tab"
>
<i class="far fa-comment-dots"></i> <i class="far fa-comment-dots"></i>
<ng-pluralize <span v-text="fmt.plural(details?.comments?.length, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></span>
count="details.comments.length"
when="{'0': 'No comments', 'one': '1 comment', 'other': '{} comments'}"
></ng-pluralize>
</button> </button>
</nav> </nav>
<div class="paper-tab-content"> <div class="paper-tab-content">
<div ng-if="details.diff && tabState.active =='diff'"> <div v-if="details?.diff &amp;&amp; tabState?.active ==&#x27;diff&#x27;">
<div class="pr-diff" ng-bind-html="details.diff | diff"></div> <div class="pr-diff" v-html="sanitize(fmt?.diff(details?.diff))"></div>
</div> </div>
<div ng-if="details.comments && tabState.active =='comments'"> <div v-if="details?.comments &amp;&amp; tabState?.active ==&#x27;comments&#x27;">
<ul class="pr-comments"> <ul class="pr-comments">
<li class="pr-comment" ng-repeat="comment in details.comments"> <li class="pr-comment" v-for="(comment, index) in details?.comments">
<div class="pr-comment-head"> <div class="pr-comment-head">
<span class="pr-comment-author" ng-if="comment.author"> <span class="pr-comment-author" v-if="comment?.author">
<i class="far fa-user"></i> @<span ng-bind="comment.author"></span> <i class="far fa-user"></i> @<span v-text="comment?.author"></span>
</span> </span>
<span class="pr-comment-date" ng-if="comment.updatedDate" ng-bind="comment.updatedDate | date"></span> <span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
</div> </div>
<div class="pr-comment-body" ng-if="comment.body"> <div class="pr-comment-body" v-if="comment?.body">
<markdown content="comment.body"></markdown> <markdown :content="comment?.body"></markdown>
</div> </div>
</li> </li>
<li class="paper-table-empty" ng-if="!details.comments.length"> <li class="paper-table-empty" v-if="!details?.comments?.length">
<i class="far fa-comment-dots"></i> <i class="far fa-comment-dots"></i>
<span>No comments on this pull request.</span> <span>No comments on this pull request.</span>
</li> </li>
+23 -28
View File
@@ -2,13 +2,13 @@
<div class="paper-crumbs">Anonymization &nbsp;/&nbsp; <span class="here">Status</span></div> <div class="paper-crumbs">Anonymization &nbsp;/&nbsp; <span class="here">Status</span></div>
<div class="d-flex align-items-end justify-content-between flex-wrap" style="gap: 12px;"> <div class="d-flex align-items-end justify-content-between flex-wrap" style="gap: 12px;">
<div> <div>
<h1 class="paper-page-title">Status of <em>{{repoId}}</em></h1> <h1 class="paper-page-title">Status of <em>{{ repoId }}</em></h1>
<p class="paper-page-lede">Track progress as your anonymization is prepared.</p> <p class="paper-page-lede">Track progress as your anonymization is prepared.</p>
</div> </div>
<span class="status-pill" ng-class="{'status-pill-ready': repo.status == 'ready', 'status-pill-error': repo.status == 'error', 'status-pill-removed': repo.status == 'removed' || repo.status == 'expired', 'status-pill-ratelimit': rateLimitResetAt}"> <span class="status-pill" :class="{&#x27;status-pill-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-pill-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-pill-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-pill-ratelimit&#x27;: rateLimitResetAt}">
<span class="status-dot" ng-class="{'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ratelimit': rateLimitResetAt}"></span> <span class="status-dot" :class="{&#x27;status-ready&#x27;: repo?.status == &#x27;ready&#x27;, &#x27;status-error&#x27;: repo?.status == &#x27;error&#x27;, &#x27;status-removed&#x27;: repo?.status == &#x27;removed&#x27; || repo?.status == &#x27;expired&#x27;, &#x27;status-ratelimit&#x27;: rateLimitResetAt}"></span>
<span ng-if="!rateLimitResetAt" ng-bind="repo.status | title"></span> <span v-if="!rateLimitResetAt" v-text="fmt?.title(repo?.status)"></span>
<span ng-if="rateLimitResetAt">Rate limited</span> <span v-if="rateLimitResetAt">Rate limited</span>
</span> </span>
</div> </div>
@@ -20,7 +20,7 @@
size. Visit the <a href="/faq">FAQ</a> for more information. size. Visit the <a href="/faq">FAQ</a> for more information.
</p> </p>
<div class="paper-ratelimit-card" ng-if="rateLimitResetAt" role="status"> <div class="paper-ratelimit-card" role="status" v-if="rateLimitResetAt">
<div class="paper-ratelimit-head"> <div class="paper-ratelimit-head">
<i class="fas fa-hourglass-half"></i> <i class="fas fa-hourglass-half"></i>
<div> <div>
@@ -28,18 +28,18 @@
<div class="paper-error-title">GitHub API rate limit reached</div> <div class="paper-error-title">GitHub API rate limit reached</div>
</div> </div>
</div> </div>
<p class="paper-error-msg">Anonymization will resume automatically in <strong>{{rateLimitCountdown}}</strong>. No action needed &mdash; the job is queued and will continue where it left off.</p> <p class="paper-error-msg">Anonymization will resume automatically in <strong>{{ rateLimitCountdown }}</strong>. No action needed &mdash; the job is queued and will continue where it left off.</p>
</div> </div>
<div class="paper-progress" ng-if="repo.status != 'error' && !rateLimitResetAt" role="progressbar" aria-valuenow="{{progress}}" aria-valuemin="0" aria-valuemax="100" ng-class="{'paper-progress-ready': repo.status == 'ready'}"> <div class="paper-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" v-if="repo?.status != &#x27;error&#x27; &amp;&amp; !rateLimitResetAt" :class="{&#x27;paper-progress-ready&#x27;: repo?.status == &#x27;ready&#x27;}" :aria-valuenow="(progress)">
<div class="paper-progress-bar" style="width: {{progress}}%;"></div> <div class="paper-progress-bar" :style="&quot;width: &quot; + (progress) + &quot;%;&quot;"></div>
<div class="paper-progress-label"> <div class="paper-progress-label">
<span ng-bind="repo.status | title"></span><span ng-if="repo.statusMessage">&nbsp;&middot;&nbsp;<span ng-bind="repo.statusMessage"></span></span> <span v-text="fmt?.title(repo?.status)"></span><span v-if="repo?.statusMessage">&nbsp;&middot;&nbsp;<span v-text="repo?.statusMessage"></span></span>
<span class="paper-progress-pct">{{progress || 0}}%</span> <span class="paper-progress-pct">{{ progress || 0 }}%</span>
</div> </div>
</div> </div>
<div class="paper-error-card" ng-if="repo.status == 'error' && !rateLimitResetAt" role="alert"> <div class="paper-error-card" role="alert" v-if="repo?.status == &#x27;error&#x27; &amp;&amp; !rateLimitResetAt">
<div class="paper-error-head"> <div class="paper-error-head">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
<div> <div>
@@ -47,15 +47,15 @@
<div class="paper-error-title">Something went wrong while preparing this repository.</div> <div class="paper-error-title">Something went wrong while preparing this repository.</div>
</div> </div>
</div> </div>
<p class="paper-error-msg" ng-if="repo.statusMessage">{{ 'ERRORS.' + repo.statusMessage | translate }}</p> <p class="paper-error-msg" v-if="repo?.statusMessage">{{ fmt?.translate('ERRORS.' + repo?.statusMessage) }}</p>
<p class="paper-error-msg" ng-if="!repo.statusMessage">No additional details were reported. The most common causes are private repositories, missing branches, and rate limits.</p> <p class="paper-error-msg" v-if="!repo?.statusMessage">No additional details were reported. The most common causes are private repositories, missing branches, and rate limits.</p>
<ul class="paper-error-hints"> <ul class="paper-error-hints">
<li>Make sure the source URL points to a repository or pull request you can access.</li> <li>Make sure the source URL points to a repository or pull request you can access.</li>
<li>Check that the chosen branch and commit still exist on GitHub.</li> <li>Check that the chosen branch and commit still exist on GitHub.</li>
<li>If you just signed in, the access token may need a moment to propagate &mdash; try again.</li> <li>If you just signed in, the access token may need a moment to propagate &mdash; try again.</li>
</ul> </ul>
<div class="paper-error-actions"> <div class="paper-error-actions">
<a class="btn btn-ink" ng-href="/anonymize/{{repoId}}"><i class="far fa-edit mr-1"></i> Edit anonymization</a> <a class="btn btn-ink" :href="safeUrl(&quot;/anonymize/&quot; + (repoId))"><i class="far fa-edit mr-1"></i> Edit anonymization</a>
<a class="btn" href="/faq"><i class="far fa-question-circle mr-1"></i> Read the FAQ</a> <a class="btn" href="/faq"><i class="far fa-question-circle mr-1"></i> Read the FAQ</a>
<a class="btn" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><i class="fas fa-bug mr-1" aria-hidden="true"></i> Report a bug in Anonymous GitHub</a> <a class="btn" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><i class="fas fa-bug mr-1" aria-hidden="true"></i> Report a bug in Anonymous GitHub</a>
</div> </div>
@@ -64,19 +64,19 @@
<div class="paper-detail-grid"> <div class="paper-detail-grid">
<div class="detail-label">Repository</div> <div class="detail-label">Repository</div>
<div class="detail-value"> <div class="detail-value">
<a href="/r/{{repoId}}/" target="_self">/r/{{repoId}}/</a> <a target="_self" :href="safeUrl(&quot;/r/&quot; + (repoId) + &quot;/&quot;)">/r/{{ repoId }}/</a>
</div> </div>
<div class="detail-label" ng-if="repo.options.page">GitHub Page</div> <div class="detail-label" v-if="repo?.options?.page">GitHub Page</div>
<div class="detail-value" ng-if="repo.options.page"> <div class="detail-value" v-if="repo?.options?.page">
<a href="/w/{{repoId}}/" target="_self">/w/{{repoId}}/</a> <a target="_self" :href="safeUrl(&quot;/w/&quot; + (repoId) + &quot;/&quot;)">/w/{{ repoId }}/</a>
</div> </div>
</div> </div>
<div class="anonymize-submit-bar" ng-if="repo.status == 'ready'"> <div class="anonymize-submit-bar" v-if="repo?.status == &#x27;ready&#x27;">
<a class="btn btn-ink" href="/r/{{repoId}}/" target="_self"> <a class="btn btn-ink" target="_self" :href="safeUrl(&quot;/r/&quot; + (repoId) + &quot;/&quot;)">
<i class="far fa-eye mr-1"></i> Go to anonymized repository <i class="far fa-eye mr-1"></i> Go to anonymized repository
</a> </a>
<a class="btn" href="/w/{{repoId}}/" target="_self" ng-if="repo.options.page"> <a class="btn" target="_self" v-if="repo?.options?.page" :href="safeUrl(&quot;/w/&quot; + (repoId) + &quot;/&quot;)">
<i class="fas fa-globe mr-1"></i> Go to anonymized GitHub page <i class="fas fa-globe mr-1"></i> Go to anonymized GitHub page
</a> </a>
</div> </div>
@@ -108,12 +108,7 @@
</div> </div>
<div class="paper-kofi-wrap"> <div class="paper-kofi-wrap">
<iframe <iframe id="kofiframe" src="https://ko-fi.com/tdurieux/?hidefeed=true&amp;widget=true&amp;embed=true&amp;preview=true" title="tdurieux" loading="lazy"></iframe>
id="kofiframe"
src="https://ko-fi.com/tdurieux/?hidefeed=true&widget=true&embed=true&preview=true"
title="tdurieux"
loading="lazy"
></iframe>
</div> </div>
</section> </section>
</div> </div>
+549 -572
View File
File diff suppressed because it is too large Load Diff
+872 -2128
View File
File diff suppressed because it is too large Load Diff
-2625
View File
File diff suppressed because one or more lines are too long
+118
View File
@@ -0,0 +1,118 @@
import { loadLibrary, loadEditor } from "./lazy-assets.js";
import { h, ref, watch, onMounted, onBeforeUnmount, nextTick, defineAsyncComponent } from "vue";
import HtmlDoc from "./html-doc.js";
import PdfViewer from "./pdf-viewer.js";
const Markdown = {
props: ["content", "terms", "options"],
setup(props) {
const host = ref(null);
const render = () => {
host.value.innerHTML = renderMD(props.content || "", window.location.pathname + "/../");
};
onMounted(() => { render(); watch(() => [props.content, props.terms, props.options], render, { deep: true }); });
return () => h("markdown", { ref: host });
},
};
const GistFile = {
props: ["file", "terms", "options"],
setup(props) {
const host = ref(null);
const highlight = () => nextTick(() => host.value?.querySelectorAll("pre code").forEach(el => window.Prism?.highlightElement(el)));
onMounted(highlight);
watch(() => [props.file?.content, props.terms, props.options], highlight, { deep: true });
return () => {
const file = props.file || {};
const extension = (file.filename || "").split(".").pop().toLowerCase();
if (["md", "markdown"].includes(extension) || file.language === "Markdown") {
return h("gist-file", { ref: host }, h(Markdown, { content: file.content, terms: props.terms, options: props.options }));
}
const aliases = { js: "javascript", jsx: "javascript", ts: "javascript", typescript: "javascript", py: "python", html: "markup", xml: "markup", svg: "markup", sh: "bash" };
const language = (file.language || extension || "none").toLowerCase();
return h("gist-file", { ref: host }, h("pre", { class: "line-numbers" }, h("code", { class: "language-" + (aliases[language] || language), key: file.content }, file.content || "")));
};
},
};
const Notebook = {
props: ["file", "content"],
setup(props) {
const host = ref(null);
let generation = 0;
let request;
const render = async () => {
const current = ++generation;
request?.abort();
request = new AbortController();
try {
const json = props.content ? JSON.parse(props.content) : await fetch(props.file?.download_url || props.file, { signal: request.signal }).then(r => { if (!r.ok) throw Error("Notebook request failed"); return r.json(); });
await loadLibrary("notebook");
if (current !== generation) return;
host.value.innerHTML = DOMPurify.sanitize(nb.parse(json).render());
host.value.querySelectorAll("pre code").forEach(el => window.Prism?.highlightElement(el));
} catch (error) { if (current === generation && error.name !== "AbortError") host.value.textContent = "Unable to render the notebook."; }
};
onMounted(() => { render(); watch(() => [props.file, props.content], render); });
onBeforeUnmount(() => { generation++; request?.abort(); });
return () => h("notebook", { ref: host });
},
};
const Loc = {
props: ["stats"],
setup(props) {
return () => {
const rows = Object.entries(props.stats || {}).filter(([, stat]) => stat.code);
const total = rows.reduce((sum, [, stat]) => sum + stat.code, 0);
return h("loc", rows.map(([language, stat]) => h("div", { class: "lang", title: `${language}: ${stat.code.toLocaleString()} lines`, style: { width: stat.code * 100 / total + "%", background: langColors[language] } })));
};
},
};
export const components = { Markdown, GistFile, Notebook, Loc, HtmlDoc, Pdfviewer: defineAsyncComponent(async () => {
await loadLibrary("pdf");
pdfjsLib.GlobalWorkerOptions.workerSrc = "/script/external/pdf.worker.js";
return PdfViewer;
}) };
export const codeEditor = {
async mounted(el, { value }) {
el._editorValue = value;
try {
await loadEditor();
if (el._editorDisposed) return;
const latest = el._editorValue;
const editor = ace.edit(el);
el._editor = editor;
editor.setValue(String(latest.content ?? ""), -1);
applyEditorOptions(el, latest.options);
latest.options?.onLoad?.(editor);
} catch (error) {
if (!el._editorDisposed) el.textContent = error.message;
}
},
updated(el, { value }) {
el._editorValue = value;
if (!el._editor) return;
if (el._editor.getValue() !== String(value.content ?? "")) el._editor.setValue(String(value.content ?? ""), -1);
applyEditorOptions(el, value.options);
},
beforeUnmount(el) { el._editorDisposed = true; el._editor?.destroy(); },
};
function applyEditorOptions(el, options = {}) {
if (options.mode) el._editor.session.setMode("ace/mode/" + options.mode);
if (options.theme) el._editor.setTheme("ace/theme/" + options.theme);
el._editor.setReadOnly(options.readOnly !== false);
}
export const paperScrollspy = {
mounted(el) {
if (!window.IntersectionObserver) return;
const links = [...el.querySelectorAll('a[href^="#"]')];
const visible = new Set();
const observer = new IntersectionObserver(entries => {
entries.forEach(e => e.isIntersecting ? visible.add(e.target.id) : visible.delete(e.target.id));
const active = links.find(a => visible.has(a.hash.slice(1)));
links.forEach(a => a.classList.toggle("active", a === active));
}, { rootMargin: "-20% 0px -60% 0px", threshold: 0 });
nextTick(() => links.forEach(a => { const target = document.getElementById(a.hash.slice(1)); if (target) observer.observe(target); }));
el._observer = observer;
},
beforeUnmount(el) { el._observer?.disconnect(); },
};
+3 -3
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
-59
View File
@@ -1,59 +0,0 @@
/*
AngularJS v1.8.2
(c) 2010-2020 Google LLC. http://angularjs.org
License: MIT
*/
(function(Y,z){'use strict';function Fa(a,b,c){if(!a)throw Pa("areq",b||"?",c||"required");return a}function Ga(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;Z(a)&&(a=a.join(" "));Z(b)&&(b=b.join(" "));return a+" "+b}function Qa(a){var b={};a&&(a.to||a.from)&&(b.to=a.to,b.from=a.from);return b}function $(a,b,c){var d="";a=Z(a)?a:a&&G(a)&&a.length?a.split(/\s+/):[];s(a,function(a,k){a&&0<a.length&&(d+=0<k?" ":"",d+=c?b+a:a+b)});return d}function Ha(a){if(a instanceof A)switch(a.length){case 0:return a;
case 1:if(1===a[0].nodeType)return a;break;default:return A(va(a))}if(1===a.nodeType)return A(a)}function va(a){if(!a[0])return a;for(var b=0;b<a.length;b++){var c=a[b];if(1===c.nodeType)return c}}function Ra(a,b,c){s(b,function(b){a.addClass(b,c)})}function Sa(a,b,c){s(b,function(b){a.removeClass(b,c)})}function aa(a){return function(b,c){c.addClass&&(Ra(a,b,c.addClass),c.addClass=null);c.removeClass&&(Sa(a,b,c.removeClass),c.removeClass=null)}}function pa(a){a=a||{};if(!a.$$prepared){var b=a.domOperation||
N;a.domOperation=function(){a.$$domOperationFired=!0;b();b=N};a.$$prepared=!0}return a}function ha(a,b){Ia(a,b);Ja(a,b)}function Ia(a,b){b.from&&(a.css(b.from),b.from=null)}function Ja(a,b){b.to&&(a.css(b.to),b.to=null)}function T(a,b,c){var d=b.options||{};c=c.options||{};var f=(d.addClass||"")+" "+(c.addClass||""),k=(d.removeClass||"")+" "+(c.removeClass||"");a=Ta(a.attr("class"),f,k);c.preparationClasses&&(d.preparationClasses=ba(c.preparationClasses,d.preparationClasses),delete c.preparationClasses);
f=d.domOperation!==N?d.domOperation:null;wa(d,c);f&&(d.domOperation=f);d.addClass=a.addClass?a.addClass:null;d.removeClass=a.removeClass?a.removeClass:null;b.addClass=d.addClass;b.removeClass=d.removeClass;return d}function Ta(a,b,c){function d(a){G(a)&&(a=a.split(" "));var c={};s(a,function(a){a.length&&(c[a]=!0)});return c}var f={};a=d(a);b=d(b);s(b,function(a,c){f[c]=1});c=d(c);s(c,function(a,c){f[c]=1===f[c]?null:-1});var k={addClass:"",removeClass:""};s(f,function(c,b){var d,f;1===c?(d="addClass",
f=!a[b]||a[b+"-remove"]):-1===c&&(d="removeClass",f=a[b]||a[b+"-add"]);f&&(k[d].length&&(k[d]+=" "),k[d]+=b)});return k}function K(a){return a instanceof A?a[0]:a}function Ua(a,b,c,d){a="";c&&(a=$(c,"ng-",!0));d.addClass&&(a=ba(a,$(d.addClass,"-add")));d.removeClass&&(a=ba(a,$(d.removeClass,"-remove")));a.length&&(d.preparationClasses=a,b.addClass(a))}function xa(a,b){var c=b?"paused":"",d=ca+"PlayState";ma(a,[d,c]);return[d,c]}function ma(a,b){a.style[b[0]]=b[1]}function ba(a,b){return a?b?a+" "+
b:a:b}function Ka(a,b,c){var d=Object.create(null),f=a.getComputedStyle(b)||{};s(c,function(a,c){var b=f[a];if(b){var L=b.charAt(0);if("-"===L||"+"===L||0<=L)b=Va(b);0===b&&(b=null);d[c]=b}});return d}function Va(a){var b=0;a=a.split(/\s*,\s*/);s(a,function(a){"s"===a.charAt(a.length-1)&&(a=a.substring(0,a.length-1));a=parseFloat(a)||0;b=b?Math.max(a,b):a});return b}function ya(a){return 0===a||null!=a}function La(a,b){var c=M,d=a+"s";b?c+="Duration":d+=" linear all";return[c,d]}function Ma(a,b,c){s(c,
function(c){a[c]=za(a[c])?a[c]:b.style.getPropertyValue(c)})}var M,Aa,ca,Ba;void 0===Y.ontransitionend&&void 0!==Y.onwebkittransitionend?(M="WebkitTransition",Aa="webkitTransitionEnd transitionend"):(M="transition",Aa="transitionend");void 0===Y.onanimationend&&void 0!==Y.onwebkitanimationend?(ca="WebkitAnimation",Ba="webkitAnimationEnd animationend"):(ca="animation",Ba="animationend");var qa=ca+"Delay",Ca=ca+"Duration",na=M+"Delay",Na=M+"Duration",Pa=z.$$minErr("ng"),ra={blockTransitions:function(a,
b){var c=b?"-"+b+"s":"";ma(a,[na,c]);return[na,c]}},Wa={transitionDuration:Na,transitionDelay:na,transitionProperty:M+"Property",animationDuration:Ca,animationDelay:qa,animationIterationCount:ca+"IterationCount"},Xa={transitionDuration:Na,transitionDelay:na,animationDuration:Ca,animationDelay:qa},Da,wa,s,Z,za,sa,Ea,ta,G,R,A,N;z.module("ngAnimate",[],function(){N=z.noop;Da=z.copy;wa=z.extend;A=z.element;s=z.forEach;Z=z.isArray;G=z.isString;ta=z.isObject;R=z.isUndefined;za=z.isDefined;Ea=z.isFunction;
sa=z.isElement}).info({angularVersion:"1.8.2"}).directive("ngAnimateSwap",["$animate",function(a){return{restrict:"A",transclude:"element",terminal:!0,priority:550,link:function(b,c,d,f,k){var e,Q;b.$watchCollection(d.ngAnimateSwap||d["for"],function(b){e&&a.leave(e);Q&&(Q.$destroy(),Q=null);(b||0===b)&&k(function(b,d){e=b;Q=d;a.enter(b,null,c)})})}}}]).directive("ngAnimateChildren",["$interpolate",function(a){return{link:function(b,c,d){function f(a){c.data("$$ngAnimateChildren","on"===a||"true"===
a)}var k=d.ngAnimateChildren;G(k)&&0===k.length?c.data("$$ngAnimateChildren",!0):(f(a(k)(b)),d.$observe("ngAnimateChildren",f))}}}]).factory("$$rAFScheduler",["$$rAF",function(a){function b(a){d=d.concat(a);c()}function c(){if(d.length){for(var b=d.shift(),e=0;e<b.length;e++)b[e]();f||a(function(){f||c()})}}var d,f;d=b.queue=[];b.waitUntilQuiet=function(b){f&&f();f=a(function(){f=null;b();c()})};return b}]).provider("$$animateQueue",["$animateProvider",function(a){function b(a){return{addClass:a.addClass,
removeClass:a.removeClass,from:a.from,to:a.to}}function c(a){if(!a)return null;a=a.split(" ");var b=Object.create(null);s(a,function(a){b[a]=!0});return b}function d(a,b){if(a&&b){var d=c(b);return a.split(" ").some(function(a){return d[a]})}}function f(a,b,c){return e[a].some(function(a){return a(b,c)})}function k(a,b){var c=0<(a.addClass||"").length,d=0<(a.removeClass||"").length;return b?c&&d:c||d}var e=this.rules={skip:[],cancel:[],join:[]};e.join.push(function(a,b){return!a.structural&&k(a)});
e.skip.push(function(a,b){return!a.structural&&!k(a)});e.skip.push(function(a,b){return"leave"===b.event&&a.structural});e.skip.push(function(a,b){return b.structural&&2===b.state&&!a.structural});e.cancel.push(function(a,b){return b.structural&&a.structural});e.cancel.push(function(a,b){return 2===b.state&&a.structural});e.cancel.push(function(a,b){if(b.structural)return!1;var c=a.addClass,f=a.removeClass,k=b.addClass,e=b.removeClass;return R(c)&&R(f)||R(k)&&R(e)?!1:d(c,e)||d(f,k)});this.$get=["$$rAF",
"$rootScope","$rootElement","$document","$$Map","$$animation","$$AnimateRunner","$templateRequest","$$jqLite","$$forceReflow","$$isDocumentHidden",function(c,d,e,C,U,oa,H,u,t,I,da){function ia(a){O.delete(a.target)}function v(){var a=!1;return function(b){a?b():d.$$postDigest(function(){a=!0;b()})}}function ua(a,b,c){var g=[],l=m[c];l&&s(l,function(l){Oa.call(l.node,b)?g.push(l.callback):"leave"===c&&Oa.call(l.node,a)&&g.push(l.callback)});return g}function h(a,b,c){var l=va(b);return a.filter(function(a){return!(a.node===
l&&(!c||a.callback===c))})}function q(a,J,w){function e(a,b,l,g){u(function(){var a=ua(ia,m,b);a.length?c(function(){s(a,function(a){a(h,l,g)});"close"!==l||m.parentNode||D.off(m)}):"close"!==l||m.parentNode||D.off(m)});a.progress(b,l,g)}function I(a){var b=h,c=n;c.preparationClasses&&(b.removeClass(c.preparationClasses),c.preparationClasses=null);c.activeClasses&&(b.removeClass(c.activeClasses),c.activeClasses=null);W(h,n);ha(h,n);n.domOperation();q.complete(!a)}var n=Da(w),h=Ha(a),m=K(h),ia=m&&
m.parentNode,n=pa(n),q=new H,u=v();Z(n.addClass)&&(n.addClass=n.addClass.join(" "));n.addClass&&!G(n.addClass)&&(n.addClass=null);Z(n.removeClass)&&(n.removeClass=n.removeClass.join(" "));n.removeClass&&!G(n.removeClass)&&(n.removeClass=null);n.from&&!ta(n.from)&&(n.from=null);n.to&&!ta(n.to)&&(n.to=null);if(!(B&&m&&fa(m,J,w)&&Ya(m,n)))return I(),q;var x=0<=["enter","move","leave"].indexOf(J),r=da(),P=r||O.get(m);w=!P&&y.get(m)||{};var p=!!w.state;P||p&&1===w.state||(P=!E(m,ia,J));if(P)return r&&
e(q,J,"start",b(n)),I(),r&&e(q,J,"close",b(n)),q;x&&F(m);r={structural:x,element:h,event:J,addClass:n.addClass,removeClass:n.removeClass,close:I,options:n,runner:q};if(p){if(f("skip",r,w)){if(2===w.state)return I(),q;T(h,w,r);return w.runner}if(f("cancel",r,w))if(2===w.state)w.runner.end();else if(w.structural)w.close();else return T(h,w,r),w.runner;else if(f("join",r,w))if(2===w.state)T(h,r,{});else return Ua(t,h,x?J:null,n),J=r.event=w.event,n=T(h,w,r),w.runner}else T(h,r,{});(p=r.structural)||
(p="animate"===r.event&&0<Object.keys(r.options.to||{}).length||k(r));if(!p)return I(),g(m),q;var C=(w.counter||0)+1;r.counter=C;l(m,1,r);d.$$postDigest(function(){h=Ha(a);var c=y.get(m),d=!c,c=c||{},t=0<(h.parent()||[]).length&&("animate"===c.event||c.structural||k(c));if(d||c.counter!==C||!t){d&&(W(h,n),ha(h,n));if(d||x&&c.event!==J)n.domOperation(),q.end();t||g(m)}else J=!c.structural&&k(c,!0)?"setClass":c.event,l(m,2),c=oa(h,J,c.options),q.setHost(c),e(q,J,"start",b(n)),c.done(function(a){I(!a);
(a=y.get(m))&&a.counter===C&&g(m);e(q,J,"close",b(n))})});return q}function F(a){a=a.querySelectorAll("[data-ng-animate]");s(a,function(a){var b=parseInt(a.getAttribute("data-ng-animate"),10),c=y.get(a);if(c)switch(b){case 2:c.runner.end();case 1:y.delete(a)}})}function g(a){a.removeAttribute("data-ng-animate");y.delete(a)}function E(a,b,c){c=C[0].body;var l=K(e),g=a===c||"HTML"===a.nodeName,d=a===l,t=!1,m=O.get(a),h;for((a=A.data(a,"$ngAnimatePin"))&&(b=K(a));b;){d||(d=b===l);if(1!==b.nodeType)break;
a=y.get(b)||{};if(!t){var f=O.get(b);if(!0===f&&!1!==m){m=!0;break}else!1===f&&(m=!1);t=a.structural}if(R(h)||!0===h)a=A.data(b,"$$ngAnimateChildren"),za(a)&&(h=a);if(t&&!1===h)break;g||(g=b===c);if(g&&d)break;if(!d&&(a=A.data(b,"$ngAnimatePin"))){b=K(a);continue}b=b.parentNode}return(!t||h)&&!0!==m&&d&&g}function l(a,b,c){c=c||{};c.state=b;a.setAttribute("data-ng-animate",b);c=(b=y.get(a))?wa(b,c):c;y.set(a,c)}var y=new U,O=new U,B=null,P=d.$watch(function(){return 0===u.totalPendingRequests},function(a){a&&
(P(),d.$$postDigest(function(){d.$$postDigest(function(){null===B&&(B=!0)})}))}),m=Object.create(null);U=a.customFilter();var la=a.classNameFilter();I=function(){return!0};var fa=U||I,Ya=la?function(a,b){var c=[a.getAttribute("class"),b.addClass,b.removeClass].join(" ");return la.test(c)}:I,W=aa(t),Oa=Y.Node.prototype.contains||function(a){return this===a||!!(this.compareDocumentPosition(a)&16)},D={on:function(a,b,c){var l=va(b);m[a]=m[a]||[];m[a].push({node:l,callback:c});A(b).on("$destroy",function(){y.get(l)||
D.off(a,b,c)})},off:function(a,b,c){if(1!==arguments.length||G(arguments[0])){var l=m[a];l&&(m[a]=1===arguments.length?null:h(l,b,c))}else for(l in b=arguments[0],m)m[l]=h(m[l],b)},pin:function(a,b){Fa(sa(a),"element","not an element");Fa(sa(b),"parentElement","not an element");a.data("$ngAnimatePin",b)},push:function(a,b,c,l){c=c||{};c.domOperation=l;return q(a,b,c)},enabled:function(a,b){var c=arguments.length;if(0===c)b=!!B;else if(sa(a)){var l=K(a);if(1===c)b=!O.get(l);else{if(!O.has(l))A(a).on("$destroy",
ia);O.set(l,!b)}}else b=B=!!a;return b}};return D}]}]).provider("$$animateCache",function(){var a=0,b=Object.create(null);this.$get=[function(){return{cacheKey:function(b,d,f,k){var e=b.parentNode;b=[e.$$ngAnimateParentKey||(e.$$ngAnimateParentKey=++a),d,b.getAttribute("class")];f&&b.push(f);k&&b.push(k);return b.join(" ")},containsCachedAnimationWithoutDuration:function(a){return(a=b[a])&&!a.isValid||!1},flush:function(){b=Object.create(null)},count:function(a){return(a=b[a])?a.total:0},get:function(a){return(a=
b[a])&&a.value},put:function(a,d,f){b[a]?(b[a].total++,b[a].value=d):b[a]={total:1,value:d,isValid:f}}}}]}).provider("$$animation",["$animateProvider",function(a){var b=this.drivers=[];this.$get=["$$jqLite","$rootScope","$injector","$$AnimateRunner","$$Map","$$rAFScheduler","$$animateCache",function(a,d,f,k,e,Q,L){function x(a){function b(a){if(a.processed)return a;a.processed=!0;var d=a.domNode,t=d.parentNode;f.set(d,a);for(var h;t;){if(h=f.get(t)){h.processed||(h=b(h));break}t=t.parentNode}(h||
c).children.push(a);return a}var c={children:[]},d,f=new e;for(d=0;d<a.length;d++){var da=a[d];f.set(da.domNode,a[d]={domNode:da.domNode,element:da.element,fn:da.fn,children:[]})}for(d=0;d<a.length;d++)b(a[d]);return function(a){var b=[],c=[],d;for(d=0;d<a.children.length;d++)c.push(a.children[d]);a=c.length;var t=0,f=[];for(d=0;d<c.length;d++){var g=c[d];0>=a&&(a=t,t=0,b.push(f),f=[]);f.push(g);g.children.forEach(function(a){t++;c.push(a)});a--}f.length&&b.push(f);return b}(c)}var C=[],U=aa(a);return function(e,
H,u){function t(a){a=a.hasAttribute("ng-animate-ref")?[a]:a.querySelectorAll("[ng-animate-ref]");var b=[];s(a,function(a){var c=a.getAttribute("ng-animate-ref");c&&c.length&&b.push(a)});return b}function I(a){var b=[],c={};s(a,function(a,d){var l=K(a.element),g=0<=["enter","move"].indexOf(a.event),l=a.structural?t(l):[];if(l.length){var f=g?"to":"from";s(l,function(a){var b=a.getAttribute("ng-animate-ref");c[b]=c[b]||{};c[b][f]={animationID:d,element:A(a)}})}else b.push(a)});var d={},g={};s(c,function(c,
t){var f=c.from,e=c.to;if(f&&e){var h=a[f.animationID],k=a[e.animationID],E=f.animationID.toString();if(!g[E]){var I=g[E]={structural:!0,beforeStart:function(){h.beforeStart();k.beforeStart()},close:function(){h.close();k.close()},classes:da(h.classes,k.classes),from:h,to:k,anchors:[]};I.classes.length?b.push(I):(b.push(h),b.push(k))}g[E].anchors.push({out:f.element,"in":e.element})}else f=f?f.animationID:e.animationID,e=f.toString(),d[e]||(d[e]=!0,b.push(a[f]))});return b}function da(a,b){a=a.split(" ");
b=b.split(" ");for(var c=[],d=0;d<a.length;d++){var g=a[d];if("ng-"!==g.substring(0,3))for(var t=0;t<b.length;t++)if(g===b[t]){c.push(g);break}}return c.join(" ")}function ia(a){for(var c=b.length-1;0<=c;c--){var d=f.get(b[c])(a);if(d)return d}}function v(a,b){function c(a){(a=a.data("$$animationRunner"))&&a.setHost(b)}a.from&&a.to?(c(a.from.element),c(a.to.element)):c(a.element)}function ua(){var a=e.data("$$animationRunner");!a||"leave"===H&&u.$$domOperationFired||a.end()}function h(b){e.off("$destroy",
ua);e.removeData("$$animationRunner");U(e,u);ha(e,u);u.domOperation();E&&a.removeClass(e,E);F.complete(!b)}u=pa(u);var q=0<=["enter","move","leave"].indexOf(H),F=new k({end:function(){h()},cancel:function(){h(!0)}});if(!b.length)return h(),F;var g=Ga(e.attr("class"),Ga(u.addClass,u.removeClass)),E=u.tempClasses;E&&(g+=" "+E,u.tempClasses=null);q&&e.data("$$animatePrepareClasses","ng-"+H+"-prepare");e.data("$$animationRunner",F);C.push({element:e,classes:g,event:H,structural:q,options:u,beforeStart:function(){E=
(E?E+" ":"")+"ng-animate";a.addClass(e,E);var b=e.data("$$animatePrepareClasses");b&&a.removeClass(e,b)},close:h});e.on("$destroy",ua);if(1<C.length)return F;d.$$postDigest(function(){var b=[];s(C,function(a){a.element.data("$$animationRunner")?b.push(a):a.close()});C.length=0;var d=I(b),g=[];s(d,function(a){var b=a.from?a.from.element:a.element,c=u.addClass,d=L.cacheKey(b[0],a.event,(c?c+" ":"")+"ng-animate",u.removeClass);g.push({element:b,domNode:K(b),fn:function(){var b,c=a.close;if(L.containsCachedAnimationWithoutDuration(d))c();
else{a.beforeStart();if((a.anchors?a.from.element||a.to.element:a.element).data("$$animationRunner")){var g=ia(a);g&&(b=g.start)}b?(b=b(),b.done(function(a){c(!a)}),v(a,b)):c()}}})});for(var d=x(g),t=0;t<d.length;t++)for(var f=d[t],e=0;e<f.length;e++){var h=f[e],k=h.element;d[t][e]=h.fn;0===t?k.removeData("$$animatePrepareClasses"):(h=k.data("$$animatePrepareClasses"))&&a.addClass(k,h)}Q(d)});return F}}]}]).provider("$animateCss",["$animateProvider",function(a){this.$get=["$window","$$jqLite","$$AnimateRunner",
"$timeout","$$animateCache","$$forceReflow","$sniffer","$$rAFScheduler","$$animateQueue",function(a,c,d,f,k,e,Q,L,x){function C(d,f,e,x){var v,s="stagger-"+e;0<k.count(e)&&(v=k.get(s),v||(f=$(f,"-stagger"),c.addClass(d,f),v=Ka(a,d,x),v.animationDuration=Math.max(v.animationDuration,0),v.transitionDuration=Math.max(v.transitionDuration,0),c.removeClass(d,f),k.put(s,v,!0)));return v||{}}function U(a){u.push(a);L.waitUntilQuiet(function(){k.flush();for(var a=e(),b=0;b<u.length;b++)u[b](a);u.length=0})}
function z(c,d,f,e){d=k.get(f);d||(d=Ka(a,c,Wa),"infinite"===d.animationIterationCount&&(d.animationIterationCount=1));k.put(f,d,e||0<d.transitionDuration||0<d.animationDuration);c=d;f=c.animationDelay;e=c.transitionDelay;c.maxDelay=f&&e?Math.max(f,e):f||e;c.maxDuration=Math.max(c.animationDuration*c.animationIterationCount,c.transitionDuration);return c}var H=aa(c),u=[];return function(a,b){function e(){v()}function L(){v(!0)}function v(b){if(!(P||la&&m)){P=!0;m=!1;V&&!g.$$skipPreparationClasses&&
c.removeClass(a,V);ba&&c.removeClass(a,ba);xa(l,!1);ra.blockTransitions(l,!1);s(y,function(a){l.style[a[0]]=""});H(a,g);ha(a,g);Object.keys(E).length&&s(E,function(a,b){a?l.style.setProperty(b,a):l.style.removeProperty(b)});if(g.onDone)g.onDone();w&&w.length&&a.off(w.join(" "),q);var d=a.data("$$animateCss");d&&(f.cancel(d[0].timer),a.removeData("$$animateCss"));fa&&fa.complete(!b)}}function u(a){p.blockTransition&&ra.blockTransitions(l,a);p.blockKeyframeAnimation&&xa(l,!!a)}function h(){fa=new d({end:e,
cancel:L});U(N);v();return{$$willAnimate:!1,start:function(){return fa},end:e}}function q(a){a.stopPropagation();var b=a.originalEvent||a;b.target===l&&(a=b.$manualTimeStamp||Date.now(),b=parseFloat(b.elapsedTime.toFixed(3)),Math.max(a-J,0)>=G&&b>=D&&(la=!0,v()))}function F(){function b(){if(!P){u(!1);s(y,function(a){l.style[a[0]]=a[1]});H(a,g);c.addClass(a,ba);if(p.recalculateTimingStyles){T=l.getAttribute("class")+" "+V;ka=k.cacheKey(l,ja,g.addClass,g.removeClass);r=z(l,T,ka,!1);ga=r.maxDelay;W=
Math.max(ga,0);D=r.maxDuration;if(0===D){v();return}p.hasTransitions=0<r.transitionDuration;p.hasAnimations=0<r.animationDuration}p.applyAnimationDelay&&(ga="boolean"!==typeof g.delay&&ya(g.delay)?parseFloat(g.delay):ga,W=Math.max(ga,0),r.animationDelay=ga,ea=[qa,ga+"s"],y.push(ea),l.style[ea[0]]=ea[1]);G=1E3*W;R=1E3*D;if(g.easing){var e,h=g.easing;p.hasTransitions&&(e=M+"TimingFunction",y.push([e,h]),l.style[e]=h);p.hasAnimations&&(e=ca+"TimingFunction",y.push([e,h]),l.style[e]=h)}r.transitionDuration&&
w.push(Aa);r.animationDuration&&w.push(Ba);J=Date.now();var m=G+1.5*R;e=J+m;var h=a.data("$$animateCss")||[],F=!0;if(h.length){var n=h[0];(F=e>n.expectedEndTime)?f.cancel(n.timer):h.push(v)}F&&(m=f(d,m,!1),h[0]={timer:m,expectedEndTime:e},h.push(v),a.data("$$animateCss",h));if(w.length)a.on(w.join(" "),q);g.to&&(g.cleanupStyles&&Ma(E,l,Object.keys(g.to)),Ja(a,g))}}function d(){var b=a.data("$$animateCss");if(b){for(var c=1;c<b.length;c++)b[c]();a.removeData("$$animateCss")}}if(!P)if(l.parentNode){var e=
function(a){if(la)m&&a&&(m=!1,v());else if(m=!a,r.animationDuration)if(a=xa(l,m),m)y.push(a);else{var b=y,c=b.indexOf(a);0<=a&&b.splice(c,1)}},h=0<aa&&(r.transitionDuration&&0===X.transitionDuration||r.animationDuration&&0===X.animationDuration)&&Math.max(X.animationDelay,X.transitionDelay);h?f(b,Math.floor(h*aa*1E3),!1):b();A.resume=function(){e(!0)};A.pause=function(){e(!1)}}else v()}var g=b||{};g.$$prepared||(g=pa(Da(g)));var E={},l=K(a);if(!l||!l.parentNode||!x.enabled())return h();var y=[],O=
a.attr("class"),B=Qa(g),P,m,la,fa,A,W,G,D,R,J,w=[];if(0===g.duration||!Q.animations&&!Q.transitions)return h();var ja=g.event&&Z(g.event)?g.event.join(" "):g.event,Y=ja&&g.structural,n="",S="";Y?n=$(ja,"ng-",!0):ja&&(n=ja);g.addClass&&(S+=$(g.addClass,"-add"));g.removeClass&&(S.length&&(S+=" "),S+=$(g.removeClass,"-remove"));g.applyClassesEarly&&S.length&&H(a,g);var V=[n,S].join(" ").trim(),T=O+" "+V,O=B.to&&0<Object.keys(B.to).length;if(!(0<(g.keyframeStyle||"").length||O||V))return h();var X,ka=
k.cacheKey(l,ja,g.addClass,g.removeClass);if(k.containsCachedAnimationWithoutDuration(ka))return V=null,h();0<g.stagger?(B=parseFloat(g.stagger),X={transitionDelay:B,animationDelay:B,transitionDuration:0,animationDuration:0}):X=C(l,V,ka,Xa);g.$$skipPreparationClasses||c.addClass(a,V);g.transitionStyle&&(B=[M,g.transitionStyle],ma(l,B),y.push(B));0<=g.duration&&(B=0<l.style[M].length,B=La(g.duration,B),ma(l,B),y.push(B));g.keyframeStyle&&(B=[ca,g.keyframeStyle],ma(l,B),y.push(B));var aa=X?0<=g.staggerIndex?
g.staggerIndex:k.count(ka):0;(n=0===aa)&&!g.skipBlocking&&ra.blockTransitions(l,9999);var r=z(l,T,ka,!Y),ga=r.maxDelay;W=Math.max(ga,0);D=r.maxDuration;var p={};p.hasTransitions=0<r.transitionDuration;p.hasAnimations=0<r.animationDuration;p.hasTransitionAll=p.hasTransitions&&"all"===r.transitionProperty;p.applyTransitionDuration=O&&(p.hasTransitions&&!p.hasTransitionAll||p.hasAnimations&&!p.hasTransitions);p.applyAnimationDuration=g.duration&&p.hasAnimations;p.applyTransitionDelay=ya(g.delay)&&(p.applyTransitionDuration||
p.hasTransitions);p.applyAnimationDelay=ya(g.delay)&&p.hasAnimations;p.recalculateTimingStyles=0<S.length;if(p.applyTransitionDuration||p.applyAnimationDuration)D=g.duration?parseFloat(g.duration):D,p.applyTransitionDuration&&(p.hasTransitions=!0,r.transitionDuration=D,B=0<l.style[M+"Property"].length,y.push(La(D,B))),p.applyAnimationDuration&&(p.hasAnimations=!0,r.animationDuration=D,y.push([Ca,D+"s"]));if(0===D&&!p.recalculateTimingStyles)return h();var ba=$(V,"-active");if(null!=g.delay){var ea;
"boolean"!==typeof g.delay&&(ea=parseFloat(g.delay),W=Math.max(ea,0));p.applyTransitionDelay&&y.push([na,ea+"s"]);p.applyAnimationDelay&&y.push([qa,ea+"s"])}null==g.duration&&0<r.transitionDuration&&(p.recalculateTimingStyles=p.recalculateTimingStyles||n);G=1E3*W;R=1E3*D;g.skipBlocking||(p.blockTransition=0<r.transitionDuration,p.blockKeyframeAnimation=0<r.animationDuration&&0<X.animationDelay&&0===X.animationDuration);g.from&&(g.cleanupStyles&&Ma(E,l,Object.keys(g.from)),Ia(a,g));p.blockTransition||
p.blockKeyframeAnimation?u(D):g.skipBlocking||ra.blockTransitions(l,!1);return{$$willAnimate:!0,end:e,start:function(){if(!P)return A={end:e,cancel:L,resume:null,pause:null},fa=new d(A),U(F),fa}}}}]}]).provider("$$animateCssDriver",["$$animationProvider",function(a){a.drivers.push("$$animateCssDriver");this.$get=["$animateCss","$rootScope","$$AnimateRunner","$rootElement","$sniffer","$$jqLite","$document",function(a,c,d,f,k,e,Q){function L(a){return a.replace(/\bng-\S+\b/g,"")}function x(a,b){G(a)&&
(a=a.split(" "));G(b)&&(b=b.split(" "));return a.filter(function(a){return-1===b.indexOf(a)}).join(" ")}function C(c,e,f){function k(a){var b={},c=K(a).getBoundingClientRect();s(["width","height","top","left"],function(a){var d=c[a];switch(a){case "top":d+=H.scrollTop;break;case "left":d+=H.scrollLeft}b[a]=Math.floor(d)+"px"});return b}function v(){var c=L(f.attr("class")||""),d=x(c,q),c=x(q,c),d=a(h,{to:k(f),addClass:"ng-anchor-in "+d,removeClass:"ng-anchor-out "+c,delay:!0});return d.$$willAnimate?
d:null}function C(){h.remove();e.removeClass("ng-animate-shim");f.removeClass("ng-animate-shim")}var h=A(K(e).cloneNode(!0)),q=L(h.attr("class")||"");e.addClass("ng-animate-shim");f.addClass("ng-animate-shim");h.addClass("ng-anchor");u.append(h);var F;c=function(){var c=a(h,{addClass:"ng-anchor-out",delay:!0,from:k(e)});return c.$$willAnimate?c:null}();if(!c&&(F=v(),!F))return C();var g=c||F;return{start:function(){function a(){c&&c.end()}var b,c=g.start();c.done(function(){c=null;if(!F&&(F=v()))return c=
F.start(),c.done(function(){c=null;C();b.complete()}),c;C();b.complete()});return b=new d({end:a,cancel:a})}}}function z(a,b,c,e){var f=oa(a,N),k=oa(b,N),h=[];s(e,function(a){(a=C(c,a.out,a["in"]))&&h.push(a)});if(f||k||0!==h.length)return{start:function(){function a(){s(b,function(a){a.end()})}var b=[];f&&b.push(f.start());k&&b.push(k.start());s(h,function(a){b.push(a.start())});var c=new d({end:a,cancel:a});d.all(b,function(a){c.complete(a)});return c}}}function oa(c){var d=c.element,e=c.options||
{};c.structural&&(e.event=c.event,e.structural=!0,e.applyClassesEarly=!0,"leave"===c.event&&(e.onDone=e.domOperation));e.preparationClasses&&(e.event=ba(e.event,e.preparationClasses));c=a(d,e);return c.$$willAnimate?c:null}if(!k.animations&&!k.transitions)return N;var H=Q[0].body;c=K(f);var u=A(c.parentNode&&11===c.parentNode.nodeType||H.contains(c)?c:H);return function(a){return a.from&&a.to?z(a.from,a.to,a.classes,a.anchors):oa(a)}}]}]).provider("$$animateJs",["$animateProvider",function(a){this.$get=
["$injector","$$AnimateRunner","$$jqLite",function(b,c,d){function f(c){c=Z(c)?c:c.split(" ");for(var d=[],f={},k=0;k<c.length;k++){var s=c[k],z=a.$$registeredAnimations[s];z&&!f[s]&&(d.push(b.get(z)),f[s]=!0)}return d}var k=aa(d);return function(a,b,d,x){function C(){x.domOperation();k(a,x)}function z(a,b,d,f,e){switch(d){case "animate":b=[b,f.from,f.to,e];break;case "setClass":b=[b,t,I,e];break;case "addClass":b=[b,t,e];break;case "removeClass":b=[b,I,e];break;default:b=[b,e]}b.push(f);if(a=a.apply(a,
b))if(Ea(a.start)&&(a=a.start()),a instanceof c)a.done(e);else if(Ea(a))return a;return N}function A(a,b,d,e,f){var h=[];s(e,function(e){var l=e[f];l&&h.push(function(){var e,f,h=!1,k=function(a){h||(h=!0,(f||N)(a),e.complete(!a))};e=new c({end:function(){k()},cancel:function(){k(!0)}});f=z(l,a,b,d,function(a){k(!1===a)});return e})});return h}function H(a,b,d,e,f){var h=A(a,b,d,e,f);if(0===h.length){var k,q;"beforeSetClass"===f?(k=A(a,"removeClass",d,e,"beforeRemoveClass"),q=A(a,"addClass",d,e,"beforeAddClass")):
"setClass"===f&&(k=A(a,"removeClass",d,e,"removeClass"),q=A(a,"addClass",d,e,"addClass"));k&&(h=h.concat(k));q&&(h=h.concat(q))}if(0!==h.length)return function(a){var b=[];h.length&&s(h,function(a){b.push(a())});b.length?c.all(b,a):a();return function(a){s(b,function(b){a?b.cancel():b.end()})}}}var u=!1;3===arguments.length&&ta(d)&&(x=d,d=null);x=pa(x);d||(d=a.attr("class")||"",x.addClass&&(d+=" "+x.addClass),x.removeClass&&(d+=" "+x.removeClass));var t=x.addClass,I=x.removeClass,G=f(d),K,v;if(G.length){var M,
h;"leave"===b?(h="leave",M="afterLeave"):(h="before"+b.charAt(0).toUpperCase()+b.substr(1),M=b);"enter"!==b&&"move"!==b&&(K=H(a,b,x,G,h));v=H(a,b,x,G,M)}if(K||v){var q;return{$$willAnimate:!0,end:function(){q?q.end():(u=!0,C(),ha(a,x),q=new c,q.complete(!0));return q},start:function(){function b(c){u=!0;C();ha(a,x);q.complete(c)}if(q)return q;q=new c;var d,f=[];K&&f.push(function(a){d=K(a)});f.length?f.push(function(a){C();a(!0)}):C();v&&f.push(function(a){d=v(a)});q.setHost({end:function(){u||((d||
N)(void 0),b(void 0))},cancel:function(){u||((d||N)(!0),b(!0))}});c.chain(f,b);return q}}}}}]}]).provider("$$animateJsDriver",["$$animationProvider",function(a){a.drivers.push("$$animateJsDriver");this.$get=["$$animateJs","$$AnimateRunner",function(a,c){function d(c){return a(c.element,c.event,c.classes,c.options)}return function(a){if(a.from&&a.to){var b=d(a.from),e=d(a.to);if(b||e)return{start:function(){function a(){return function(){s(d,function(a){a.end()})}}var d=[];b&&d.push(b.start());e&&
d.push(e.start());c.all(d,function(a){f.complete(a)});var f=new c({end:a(),cancel:a()});return f}}}else return d(a)}}]}])})(window,window.angular);
//# sourceMappingURL=angular-animate.min.js.map
-17
View File
@@ -1,17 +0,0 @@
/*
AngularJS v1.8.2
(c) 2010-2020 Google LLC. http://angularjs.org
License: MIT
*/
(function(I,b){'use strict';function z(b,h){var d=[],c=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[?*])?/g,function(b,c,h,k){b="?"===k||"*?"===k;k="*"===k||"*?"===k;d.push({name:h,optional:b});c=c||"";return(b?"(?:"+c:c+"(?:")+(k?"(.+?)":"([^/]+)")+(b?"?)?":")")}).replace(/([/$*])/g,"\\$1");h.ignoreTrailingSlashes&&(c=c.replace(/\/+$/,"")+"/*");return{keys:d,regexp:new RegExp("^"+c+"(?:[?#]|$)",h.caseInsensitiveMatch?"i":"")}}function A(b){p&&b.get("$route")}function v(u,h,d){return{restrict:"ECA",
terminal:!0,priority:400,transclude:"element",link:function(c,f,g,l,k){function q(){r&&(d.cancel(r),r=null);m&&(m.$destroy(),m=null);s&&(r=d.leave(s),r.done(function(b){!1!==b&&(r=null)}),s=null)}function C(){var g=u.current&&u.current.locals;if(b.isDefined(g&&g.$template)){var g=c.$new(),l=u.current;s=k(g,function(g){d.enter(g,null,s||f).done(function(d){!1===d||!b.isDefined(w)||w&&!c.$eval(w)||h()});q()});m=l.scope=g;m.$emit("$viewContentLoaded");m.$eval(p)}else q()}var m,s,r,w=g.autoscroll,p=g.onload||
"";c.$on("$routeChangeSuccess",C);C()}}}function x(b,h,d){return{restrict:"ECA",priority:-400,link:function(c,f){var g=d.current,l=g.locals;f.html(l.$template);var k=b(f.contents());if(g.controller){l.$scope=c;var q=h(g.controller,l);g.controllerAs&&(c[g.controllerAs]=q);f.data("$ngControllerController",q);f.children().data("$ngControllerController",q)}c[g.resolveAs||"$resolve"]=l;k(c)}}}var D,E,F,G,y=b.module("ngRoute",[]).info({angularVersion:"1.8.2"}).provider("$route",function(){function u(d,
c){return b.extend(Object.create(d),c)}D=b.isArray;E=b.isObject;F=b.isDefined;G=b.noop;var h={};this.when=function(d,c){var f;f=void 0;if(D(c)){f=f||[];for(var g=0,l=c.length;g<l;g++)f[g]=c[g]}else if(E(c))for(g in f=f||{},c)if("$"!==g.charAt(0)||"$"!==g.charAt(1))f[g]=c[g];f=f||c;b.isUndefined(f.reloadOnUrl)&&(f.reloadOnUrl=!0);b.isUndefined(f.reloadOnSearch)&&(f.reloadOnSearch=!0);b.isUndefined(f.caseInsensitiveMatch)&&(f.caseInsensitiveMatch=this.caseInsensitiveMatch);h[d]=b.extend(f,{originalPath:d},
d&&z(d,f));d&&(g="/"===d[d.length-1]?d.substr(0,d.length-1):d+"/",h[g]=b.extend({originalPath:d,redirectTo:d},z(g,f)));return this};this.caseInsensitiveMatch=!1;this.otherwise=function(b){"string"===typeof b&&(b={redirectTo:b});this.when(null,b);return this};p=!0;this.eagerInstantiationEnabled=function(b){return F(b)?(p=b,this):p};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce","$browser",function(d,c,f,g,l,k,q,p){function m(a){var e=t.current;n=A();(x=
!B&&n&&e&&n.$$route===e.$$route&&(!n.reloadOnUrl||!n.reloadOnSearch&&b.equals(n.pathParams,e.pathParams)))||!e&&!n||d.$broadcast("$routeChangeStart",n,e).defaultPrevented&&a&&a.preventDefault()}function s(){var a=t.current,e=n;if(x)a.params=e.params,b.copy(a.params,f),d.$broadcast("$routeUpdate",a);else if(e||a){B=!1;t.current=e;var c=g.resolve(e);p.$$incOutstandingRequestCount("$route");c.then(r).then(w).then(function(g){return g&&c.then(y).then(function(c){e===t.current&&(e&&(e.locals=c,b.copy(e.params,
f)),d.$broadcast("$routeChangeSuccess",e,a))})}).catch(function(b){e===t.current&&d.$broadcast("$routeChangeError",e,a,b)}).finally(function(){p.$$completeOutstandingRequest(G,"$route")})}}function r(a){var e={route:a,hasRedirection:!1};if(a)if(a.redirectTo)if(b.isString(a.redirectTo))e.path=v(a.redirectTo,a.params),e.search=a.params,e.hasRedirection=!0;else{var d=c.path(),f=c.search();a=a.redirectTo(a.pathParams,d,f);b.isDefined(a)&&(e.url=a,e.hasRedirection=!0)}else if(a.resolveRedirectTo)return g.resolve(l.invoke(a.resolveRedirectTo)).then(function(a){b.isDefined(a)&&
(e.url=a,e.hasRedirection=!0);return e});return e}function w(a){var b=!0;if(a.route!==t.current)b=!1;else if(a.hasRedirection){var g=c.url(),d=a.url;d?c.url(d).replace():d=c.path(a.path).search(a.search).replace().url();d!==g&&(b=!1)}return b}function y(a){if(a){var e=b.extend({},a.resolve);b.forEach(e,function(a,c){e[c]=b.isString(a)?l.get(a):l.invoke(a,null,null,c)});a=z(a);b.isDefined(a)&&(e.$template=a);return g.all(e)}}function z(a){var e,c;b.isDefined(e=a.template)?b.isFunction(e)&&(e=e(a.params)):
b.isDefined(c=a.templateUrl)&&(b.isFunction(c)&&(c=c(a.params)),b.isDefined(c)&&(a.loadedTemplateUrl=q.valueOf(c),e=k(c)));return e}function A(){var a,e;b.forEach(h,function(d,g){var f;if(f=!e){var h=c.path();f=d.keys;var l={};if(d.regexp)if(h=d.regexp.exec(h)){for(var k=1,p=h.length;k<p;++k){var m=f[k-1],n=h[k];m&&n&&(l[m.name]=n)}f=l}else f=null;else f=null;f=a=f}f&&(e=u(d,{params:b.extend({},c.search(),a),pathParams:a}),e.$$route=d)});return e||h[null]&&u(h[null],{params:{},pathParams:{}})}function v(a,
c){var d=[];b.forEach((a||"").split(":"),function(a,b){if(0===b)d.push(a);else{var f=a.match(/(\w+)(?:[?*])?(.*)/),g=f[1];d.push(c[g]);d.push(f[2]||"");delete c[g]}});return d.join("")}var B=!1,n,x,t={routes:h,reload:function(){B=!0;var a={defaultPrevented:!1,preventDefault:function(){this.defaultPrevented=!0;B=!1}};d.$evalAsync(function(){m(a);a.defaultPrevented||s()})},updateParams:function(a){if(this.current&&this.current.$$route)a=b.extend({},this.current.params,a),c.path(v(this.current.$$route.originalPath,
a)),c.search(a);else throw H("norout");}};d.$on("$locationChangeStart",m);d.$on("$locationChangeSuccess",s);return t}]}).run(A),H=b.$$minErr("ngRoute"),p;A.$inject=["$injector"];y.provider("$routeParams",function(){this.$get=function(){return{}}});y.directive("ngView",v);y.directive("ngView",x);v.$inject=["$route","$anchorScroll","$animate"];x.$inject=["$compile","$controller","$route"]})(window,window.angular);
//# sourceMappingURL=angular-route.min.js.map
-17
View File
@@ -1,17 +0,0 @@
/*
AngularJS v1.8.2
(c) 2010-2020 Google LLC. http://angularjs.org
License: MIT
*/
(function(s,e){'use strict';function O(e){var g=[];B(g,D).chars(e);return g.join("")}var C=e.$$minErr("$sanitize"),E,g,F,G,H,q,D,I,J,B;e.module("ngSanitize",[]).provider("$sanitize",function(){function h(a,d){return A(a.split(","),d)}function A(a,d){var c={},b;for(b=0;b<a.length;b++)c[d?q(a[b]):a[b]]=!0;return c}function t(a,d){d&&d.length&&g(a,A(d))}function P(a){for(var d={},c=0,b=a.length;c<b;c++){var k=a[c];d[k.name]=k.value}return d}function K(a){return a.replace(/&/g,"&amp;").replace(Q,function(a){var c=
a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(c-55296)+(a-56320)+65536)+";"}).replace(u,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}function z(a){for(;a;){if(a.nodeType===s.Node.ELEMENT_NODE)for(var d=a.attributes,c=0,b=d.length;c<b;c++){var k=d[c],f=k.name.toLowerCase();if("xmlns:ns1"===f||0===f.lastIndexOf("ns1:",0))a.removeAttributeNode(k),c--,b--}(d=a.firstChild)&&z(d);a=v("nextSibling",a)}}function v(a,d){var c=d[a];if(c&&I.call(d,c))throw C("elclob",
d.outerHTML||d.outerText);return c}var y=!1,f=!1;this.$get=["$$sanitizeUri",function(a){y=!0;f&&g(m,l);return function(d){var c=[];J(d,B(c,function(b,c){return!/^unsafe:/.test(a(b,c))}));return c.join("")}}];this.enableSvg=function(a){return H(a)?(f=a,this):f};this.addValidElements=function(a){y||(G(a)&&(a={htmlElements:a}),t(l,a.svgElements),t(r,a.htmlVoidElements),t(m,a.htmlVoidElements),t(m,a.htmlElements));return this};this.addValidAttrs=function(a){y||g(L,A(a,!0));return this};E=e.bind;g=e.extend;
F=e.forEach;G=e.isArray;H=e.isDefined;q=e.$$lowercase;D=e.noop;J=function(a,d){null===a||void 0===a?a="":"string"!==typeof a&&(a=""+a);var c=M(a);if(!c)return"";var b=5;do{if(0===b)throw C("uinput");b--;a=c.innerHTML;c=M(a)}while(a!==c.innerHTML);for(b=c.firstChild;b;){switch(b.nodeType){case 1:d.start(b.nodeName.toLowerCase(),P(b.attributes));break;case 3:d.chars(b.textContent)}var k;if(!(k=b.firstChild)&&(1===b.nodeType&&d.end(b.nodeName.toLowerCase()),k=v("nextSibling",b),!k))for(;null==k;){b=
v("parentNode",b);if(b===c)break;k=v("nextSibling",b);1===b.nodeType&&d.end(b.nodeName.toLowerCase())}b=k}for(;b=c.firstChild;)c.removeChild(b)};B=function(a,d){var c=!1,b=E(a,a.push);return{start:function(a,f){a=q(a);!c&&w[a]&&(c=a);c||!0!==m[a]||(b("<"),b(a),F(f,function(c,f){var e=q(f),h="img"===a&&"src"===e||"background"===e;!0!==L[e]||!0===N[e]&&!d(c,h)||(b(" "),b(f),b('="'),b(K(c)),b('"'))}),b(">"))},end:function(a){a=q(a);c||!0!==m[a]||!0===r[a]||(b("</"),b(a),b(">"));a==c&&(c=!1)},chars:function(a){c||
b(K(a))}}};I=s.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)};var Q=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,u=/([^#-~ |!])/g,r=h("area,br,col,hr,img,wbr"),x=h("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),p=h("rp,rt"),n=g({},p,x),x=g({},x,h("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")),p=g({},p,h("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
l=h("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan"),w=h("script,style"),m=g({},r,x,p,n),N=h("background,cite,href,longdesc,src,xlink:href,xml:base"),n=h("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,valign,value,vspace,width"),
p=h("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan",
!0),L=g({},N,p,n),M=function(a,d){function c(b){b="<remove></remove>"+b;try{var c=(new a.DOMParser).parseFromString(b,"text/html").body;c.firstChild.remove();return c}catch(d){}}var b;try{b=!!c("")}catch(f){b=!1}if(b)return c;if(!d||!d.implementation)throw C("noinert");b=d.implementation.createHTMLDocument("inert");var e=(b.documentElement||b.getDocumentElement()).querySelector("body");return function(a){e.innerHTML=a;d.documentMode&&z(e);return e}}(s,s.document)}).info({angularVersion:"1.8.2"});
e.module("ngSanitize").filter("linky",["$sanitize",function(h){var g=/((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,t=/^mailto:/i,q=e.$$minErr("linky"),s=e.isDefined,z=e.isFunction,v=e.isObject,y=e.isString;return function(f,e,u){function r(e){e&&l.push(O(e))}function x(f,h){var g,a=p(f);l.push("<a ");for(g in a)l.push(g+'="'+a[g]+'" ');!s(e)||"target"in a||l.push('target="',e,'" ');l.push('href="',f.replace(/"/g,"&quot;"),'">');r(h);l.push("</a>")}if(null==
f||""===f)return f;if(!y(f))throw q("notstring",f);for(var p=z(u)?u:v(u)?function(){return u}:function(){return{}},n=f,l=[],w,m;f=n.match(g);)w=f[0],f[2]||f[4]||(w=(f[3]?"http://":"mailto:")+w),m=f.index,r(n.substr(0,m)),x(w,f[0].replace(t,"")),n=n.substring(m+f[0].length);r(n);return h(l.join(""))}}])})(window,window.angular);
//# sourceMappingURL=angular-sanitize.min.js.map
@@ -1,6 +0,0 @@
/*!
* angular-translate - v2.18.4 - 2021-01-14
*
* Copyright (c) 2021 The angular-translate team, Pascal Precht; Licensed MIT
*/
!function(e,i){"function"==typeof define&&define.amd?define([],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():i()}(0,function(){function e(n,a){"use strict";return function(r){if(!(r&&(angular.isArray(r.files)||angular.isString(r.prefix)&&angular.isString(r.suffix))))throw new Error("Couldn't load static files, no files and prefix or suffix specified!");r.files||(r.files=[{prefix:r.prefix,suffix:r.suffix}]);for(var e=function(e){if(!e||!angular.isString(e.prefix)||!angular.isString(e.suffix))throw new Error("Couldn't load static file, no prefix or suffix specified!");var i=[e.prefix,r.key,e.suffix].join("");return angular.isObject(r.fileMap)&&r.fileMap[i]&&(i=r.fileMap[i]),a(angular.extend({url:i,method:"GET"},r.$http)).then(function(e){return e.data},function(){return n.reject(r.key)})},i=[],t=r.files.length,f=0;f<t;f++)i.push(e({prefix:r.files[f].prefix,key:r.key,suffix:r.files[f].suffix}));return n.all(i).then(function(e){for(var i=e.length,r={},t=0;t<i;t++)for(var f in e[t])r[f]=e[t][f];return r})}}return e.$inject=["$q","$http"],angular.module("pascalprecht.translate").factory("$translateStaticFilesLoader",e),e.displayName="$translateStaticFilesLoader","pascalprecht.translate"});
File diff suppressed because one or more lines are too long
-352
View File
@@ -1,352 +0,0 @@
/*
AngularJS v1.8.2
(c) 2010-2020 Google LLC. http://angularjs.org
License: MIT
*/
(function(z){'use strict';function ve(a){if(D(a))w(a.objectMaxDepth)&&(Xb.objectMaxDepth=Yb(a.objectMaxDepth)?a.objectMaxDepth:NaN),w(a.urlErrorParamsEnabled)&&Ga(a.urlErrorParamsEnabled)&&(Xb.urlErrorParamsEnabled=a.urlErrorParamsEnabled);else return Xb}function Yb(a){return X(a)&&0<a}function F(a,b){b=b||Error;return function(){var d=arguments[0],c;c="["+(a?a+":":"")+d+"] http://errors.angularjs.org/1.8.2/"+(a?a+"/":"")+d;for(d=1;d<arguments.length;d++){c=c+(1==d?"?":"&")+"p"+(d-1)+"=";var e=encodeURIComponent,
f;f=arguments[d];f="function"==typeof f?f.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof f?"undefined":"string"!=typeof f?JSON.stringify(f):f;c+=e(f)}return new b(c)}}function za(a){if(null==a||$a(a))return!1;if(H(a)||C(a)||x&&a instanceof x)return!0;var b="length"in Object(a)&&a.length;return X(b)&&(0<=b&&b-1 in a||"function"===typeof a.item)}function r(a,b,d){var c,e;if(a)if(B(a))for(c in a)"prototype"!==c&&"length"!==c&&"name"!==c&&a.hasOwnProperty(c)&&b.call(d,a[c],c,a);else if(H(a)||
za(a)){var f="object"!==typeof a;c=0;for(e=a.length;c<e;c++)(f||c in a)&&b.call(d,a[c],c,a)}else if(a.forEach&&a.forEach!==r)a.forEach(b,d,a);else if(Pc(a))for(c in a)b.call(d,a[c],c,a);else if("function"===typeof a.hasOwnProperty)for(c in a)a.hasOwnProperty(c)&&b.call(d,a[c],c,a);else for(c in a)ta.call(a,c)&&b.call(d,a[c],c,a);return a}function Qc(a,b,d){for(var c=Object.keys(a).sort(),e=0;e<c.length;e++)b.call(d,a[c[e]],c[e]);return c}function Zb(a){return function(b,d){a(d,b)}}function we(){return++qb}
function $b(a,b,d){for(var c=a.$$hashKey,e=0,f=b.length;e<f;++e){var g=b[e];if(D(g)||B(g))for(var k=Object.keys(g),h=0,l=k.length;h<l;h++){var m=k[h],p=g[m];d&&D(p)?ha(p)?a[m]=new Date(p.valueOf()):ab(p)?a[m]=new RegExp(p):p.nodeName?a[m]=p.cloneNode(!0):ac(p)?a[m]=p.clone():"__proto__"!==m&&(D(a[m])||(a[m]=H(p)?[]:{}),$b(a[m],[p],!0)):a[m]=p}}c?a.$$hashKey=c:delete a.$$hashKey;return a}function S(a){return $b(a,Ha.call(arguments,1),!1)}function xe(a){return $b(a,Ha.call(arguments,1),!0)}function fa(a){return parseInt(a,
10)}function bc(a,b){return S(Object.create(a),b)}function E(){}function Ta(a){return a}function ia(a){return function(){return a}}function cc(a){return B(a.toString)&&a.toString!==la}function A(a){return"undefined"===typeof a}function w(a){return"undefined"!==typeof a}function D(a){return null!==a&&"object"===typeof a}function Pc(a){return null!==a&&"object"===typeof a&&!Rc(a)}function C(a){return"string"===typeof a}function X(a){return"number"===typeof a}function ha(a){return"[object Date]"===la.call(a)}
function H(a){return Array.isArray(a)||a instanceof Array}function dc(a){switch(la.call(a)){case "[object Error]":return!0;case "[object Exception]":return!0;case "[object DOMException]":return!0;default:return a instanceof Error}}function B(a){return"function"===typeof a}function ab(a){return"[object RegExp]"===la.call(a)}function $a(a){return a&&a.window===a}function bb(a){return a&&a.$evalAsync&&a.$watch}function Ga(a){return"boolean"===typeof a}function ye(a){return a&&X(a.length)&&ze.test(la.call(a))}
function ac(a){return!(!a||!(a.nodeName||a.prop&&a.attr&&a.find))}function Ae(a){var b={};a=a.split(",");var d;for(d=0;d<a.length;d++)b[a[d]]=!0;return b}function ua(a){return K(a.nodeName||a[0]&&a[0].nodeName)}function cb(a,b){var d=a.indexOf(b);0<=d&&a.splice(d,1);return d}function Ia(a,b,d){function c(a,b,c){c--;if(0>c)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f<g;f++)b.push(e(a[f],c))}else if(Pc(a))for(f in a)b[f]=e(a[f],c);else if(a&&"function"===typeof a.hasOwnProperty)for(f in a)a.hasOwnProperty(f)&&
(b[f]=e(a[f],c));else for(f in a)ta.call(a,f)&&(b[f]=e(a[f],c));d?b.$$hashKey=d:delete b.$$hashKey;return b}function e(a,b){if(!D(a))return a;var d=g.indexOf(a);if(-1!==d)return k[d];if($a(a)||bb(a))throw oa("cpws");var d=!1,e=f(a);void 0===e&&(e=H(a)?[]:Object.create(Rc(a)),d=!0);g.push(a);k.push(e);return d?c(a,e,b):e}function f(a){switch(la.call(a)){case "[object Int8Array]":case "[object Int16Array]":case "[object Int32Array]":case "[object Float32Array]":case "[object Float64Array]":case "[object Uint8Array]":case "[object Uint8ClampedArray]":case "[object Uint16Array]":case "[object Uint32Array]":return new a.constructor(e(a.buffer),
a.byteOffset,a.length);case "[object ArrayBuffer]":if(!a.slice){var b=new ArrayBuffer(a.byteLength);(new Uint8Array(b)).set(new Uint8Array(a));return b}return a.slice(0);case "[object Boolean]":case "[object Number]":case "[object String]":case "[object Date]":return new a.constructor(a.valueOf());case "[object RegExp]":return b=new RegExp(a.source,a.toString().match(/[^/]*$/)[0]),b.lastIndex=a.lastIndex,b;case "[object Blob]":return new a.constructor([a],{type:a.type})}if(B(a.cloneNode))return a.cloneNode(!0)}
var g=[],k=[];d=Yb(d)?d:NaN;if(b){if(ye(b)||"[object ArrayBuffer]"===la.call(b))throw oa("cpta");if(a===b)throw oa("cpi");H(b)?b.length=0:r(b,function(a,c){"$$hashKey"!==c&&delete b[c]});g.push(a);k.push(b);return c(a,b,d)}return e(a,d)}function ec(a,b){return a===b||a!==a&&b!==b}function va(a,b){if(a===b)return!0;if(null===a||null===b)return!1;if(a!==a&&b!==b)return!0;var d=typeof a,c;if(d===typeof b&&"object"===d)if(H(a)){if(!H(b))return!1;if((d=a.length)===b.length){for(c=0;c<d;c++)if(!va(a[c],
b[c]))return!1;return!0}}else{if(ha(a))return ha(b)?ec(a.getTime(),b.getTime()):!1;if(ab(a))return ab(b)?a.toString()===b.toString():!1;if(bb(a)||bb(b)||$a(a)||$a(b)||H(b)||ha(b)||ab(b))return!1;d=T();for(c in a)if("$"!==c.charAt(0)&&!B(a[c])){if(!va(a[c],b[c]))return!1;d[c]=!0}for(c in b)if(!(c in d)&&"$"!==c.charAt(0)&&w(b[c])&&!B(b[c]))return!1;return!0}return!1}function db(a,b,d){return a.concat(Ha.call(b,d))}function Va(a,b){var d=2<arguments.length?Ha.call(arguments,2):[];return!B(b)||b instanceof
RegExp?b:d.length?function(){return arguments.length?b.apply(a,db(d,arguments,0)):b.apply(a,d)}:function(){return arguments.length?b.apply(a,arguments):b.call(a)}}function Sc(a,b){var d=b;"string"===typeof a&&"$"===a.charAt(0)&&"$"===a.charAt(1)?d=void 0:$a(b)?d="$WINDOW":b&&z.document===b?d="$DOCUMENT":bb(b)&&(d="$SCOPE");return d}function eb(a,b){if(!A(a))return X(b)||(b=b?2:null),JSON.stringify(a,Sc,b)}function Tc(a){return C(a)?JSON.parse(a):a}function fc(a,b){a=a.replace(Be,"");var d=Date.parse("Jan 01, 1970 00:00:00 "+
a)/6E4;return Y(d)?b:d}function Uc(a,b){a=new Date(a.getTime());a.setMinutes(a.getMinutes()+b);return a}function gc(a,b,d){d=d?-1:1;var c=a.getTimezoneOffset();b=fc(b,c);return Uc(a,d*(b-c))}function Aa(a){a=x(a).clone().empty();var b=x("<div></div>").append(a).html();try{return a[0].nodeType===Pa?K(b):b.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+K(b)})}catch(d){return K(b)}}function Vc(a){try{return decodeURIComponent(a)}catch(b){}}function hc(a){var b={};r((a||"").split("&"),
function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Vc(e),w(e)&&(f=w(f)?Vc(f):!0,ta.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Ce(a){var b=[];r(a,function(a,c){H(a)?r(a,function(a){b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))}):b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))});return b.length?b.join("&"):""}function ic(a){return ba(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ba(a,
b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function De(a,b){var d,c,e=Qa.length;for(c=0;c<e;++c)if(d=Qa[c]+b,C(d=a.getAttribute(d)))return d;return null}function Ee(a,b){var d,c,e={};r(Qa,function(b){b+="app";!d&&a.hasAttribute&&a.hasAttribute(b)&&(d=a,c=a.getAttribute(b))});r(Qa,function(b){b+="app";var e;!d&&(e=a.querySelector("["+b.replace(":","\\:")+"]"))&&(d=e,c=e.getAttribute(b))});
d&&(Fe?(e.strictDi=null!==De(d,"strict-di"),b(d,c?[c]:[],e)):z.console.error("AngularJS: disabling automatic bootstrap. <script> protocol indicates an extension, document.location.href does not match."))}function Wc(a,b,d){D(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=x(a);if(a.injector()){var c=a[0]===z.document?"document":Aa(a);throw oa("btstrpd",c.replace(/</,"&lt;").replace(/>/,"&gt;"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",
function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=fb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;z&&e.test(z.name)&&(d.debugInfoEnabled=!0,z.name=z.name.replace(e,""));if(z&&!f.test(z.name))return c();z.name=z.name.replace(f,"");ca.resumeBootstrap=function(a){r(a,function(a){b.push(a)});return c()};B(ca.resumeDeferredBootstrap)&&
ca.resumeDeferredBootstrap()}function Ge(){z.name="NG_ENABLE_DEBUG_INFO!"+z.name;z.location.reload()}function He(a){a=ca.element(a).injector();if(!a)throw oa("test");return a.get("$$testability")}function Xc(a,b){b=b||"_";return a.replace(Ie,function(a,c){return(c?b:"")+a.toLowerCase()})}function Je(){var a;if(!Yc){var b=rb();(sb=A(b)?z.jQuery:b?z[b]:void 0)&&sb.fn.on?(x=sb,S(sb.fn,{scope:Wa.scope,isolateScope:Wa.isolateScope,controller:Wa.controller,injector:Wa.injector,inheritedData:Wa.inheritedData})):
x=U;a=x.cleanData;x.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=(x._data(f)||{}).events)&&c.$destroy&&x(f).triggerHandler("$destroy");a(b)};ca.element=x;Yc=!0}}function Ke(){U.legacyXHTMLReplacement=!0}function gb(a,b,d){if(!a)throw oa("areq",b||"?",d||"required");return a}function tb(a,b,d){d&&H(a)&&(a=a[a.length-1]);gb(B(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ja(a,b){if("hasOwnProperty"===a)throw oa("badname",
b);}function Le(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g<f;g++)c=b[g],a&&(a=(e=a)[c]);return!d&&B(a)?Va(e,a):a}function ub(a){for(var b=a[0],d=a[a.length-1],c,e=1;b!==d&&(b=b.nextSibling);e++)if(c||a[e]!==b)c||(c=x(Ha.call(a,0,e))),c.push(b);return c||a}function T(){return Object.create(null)}function jc(a){if(null==a)return"";switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=!cc(a)||H(a)||ha(a)?eb(a):a.toString()}return a}function Me(a){function b(a,
b,c){return a[b]||(a[b]=c())}var d=F("$injector"),c=F("ng");a=b(a,"angular",Object);a.$$minErr=a.$$minErr||F;return b(a,"module",function(){var a={};return function(f,g,k){var h={};if("hasOwnProperty"===f)throw c("badname","module");g&&a.hasOwnProperty(f)&&(a[f]=null);return b(a,f,function(){function a(b,c,d,f){f||(f=e);return function(){f[d||"push"]([b,c,arguments]);return t}}function b(a,c,d){d||(d=e);return function(b,e){e&&B(e)&&(e.$$moduleName=f);d.push([a,c,arguments]);return t}}if(!g)throw d("nomod",
f);var e=[],n=[],s=[],G=a("$injector","invoke","push",n),t={_invokeQueue:e,_configBlocks:n,_runBlocks:s,info:function(a){if(w(a)){if(!D(a))throw c("aobj","value");h=a;return this}return h},requires:g,name:f,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decorator:b("$provide","decorator",n),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider",
"register"),directive:b("$compileProvider","directive"),component:b("$compileProvider","component"),config:G,run:function(a){s.push(a);return this}};k&&G(k);return t})}})}function ja(a,b){if(H(a)){b=b||[];for(var d=0,c=a.length;d<c;d++)b[d]=a[d]}else if(D(a))for(d in b=b||{},a)if("$"!==d.charAt(0)||"$"!==d.charAt(1))b[d]=a[d];return b||a}function Ne(a,b){var d=[];Yb(b)&&(a=ca.copy(a,null,b));return JSON.stringify(a,function(a,b){b=Sc(a,b);if(D(b)){if(0<=d.indexOf(b))return"...";d.push(b)}return b})}
function Oe(a){S(a,{errorHandlingConfig:ve,bootstrap:Wc,copy:Ia,extend:S,merge:xe,equals:va,element:x,forEach:r,injector:fb,noop:E,bind:Va,toJson:eb,fromJson:Tc,identity:Ta,isUndefined:A,isDefined:w,isString:C,isFunction:B,isObject:D,isNumber:X,isElement:ac,isArray:H,version:Pe,isDate:ha,callbacks:{$$counter:0},getTestability:He,reloadWithDebugInfo:Ge,UNSAFE_restoreLegacyJqLiteXHTMLReplacement:Ke,$$minErr:F,$$csp:Ba,$$encodeUriSegment:ic,$$encodeUriQuery:ba,$$lowercase:K,$$stringify:jc,$$uppercase:vb});
lc=Me(z);lc("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:Qe});a.provider("$compile",Zc).directive({a:Re,input:$c,textarea:$c,form:Se,script:Te,select:Ue,option:Ve,ngBind:We,ngBindHtml:Xe,ngBindTemplate:Ye,ngClass:Ze,ngClassEven:$e,ngClassOdd:af,ngCloak:bf,ngController:cf,ngForm:df,ngHide:ef,ngIf:ff,ngInclude:gf,ngInit:hf,ngNonBindable:jf,ngPluralize:kf,ngRef:lf,ngRepeat:mf,ngShow:nf,ngStyle:of,ngSwitch:pf,ngSwitchWhen:qf,ngSwitchDefault:rf,ngOptions:sf,ngTransclude:tf,ngModel:uf,
ngList:vf,ngChange:wf,pattern:ad,ngPattern:ad,required:bd,ngRequired:bd,minlength:cd,ngMinlength:cd,maxlength:dd,ngMaxlength:dd,ngValue:xf,ngModelOptions:yf}).directive({ngInclude:zf,input:Af}).directive(wb).directive(ed);a.provider({$anchorScroll:Bf,$animate:Cf,$animateCss:Df,$$animateJs:Ef,$$animateQueue:Ff,$$AnimateRunner:Gf,$$animateAsyncRun:Hf,$browser:If,$cacheFactory:Jf,$controller:Kf,$document:Lf,$$isDocumentHidden:Mf,$exceptionHandler:Nf,$filter:fd,$$forceReflow:Of,$interpolate:Pf,$interval:Qf,
$$intervalFactory:Rf,$http:Sf,$httpParamSerializer:Tf,$httpParamSerializerJQLike:Uf,$httpBackend:Vf,$xhrFactory:Wf,$jsonpCallbacks:Xf,$location:Yf,$log:Zf,$parse:$f,$rootScope:ag,$q:bg,$$q:cg,$sce:dg,$sceDelegate:eg,$sniffer:fg,$$taskTrackerFactory:gg,$templateCache:hg,$templateRequest:ig,$$testability:jg,$timeout:kg,$window:lg,$$rAF:mg,$$jqLite:ng,$$Map:og,$$cookieReader:pg})}]).info({angularVersion:"1.8.2"})}function xb(a,b){return b.toUpperCase()}function yb(a){return a.replace(qg,xb)}function mc(a){a=
a.nodeType;return 1===a||!a||9===a}function gd(a,b){var d,c,e,f=b.createDocumentFragment(),g=[],k;if(nc.test(a)){d=f.appendChild(b.createElement("div"));c=(rg.exec(a)||["",""])[1].toLowerCase();e=U.legacyXHTMLReplacement?a.replace(sg,"<$1></$2>"):a;if(10>wa)for(c=hb[c]||hb._default,d.innerHTML=c[1]+e+c[2],k=c[0];k--;)d=d.firstChild;else{c=qa[c]||[];for(k=c.length;-1<--k;)d.appendChild(z.document.createElement(c[k])),d=d.firstChild;d.innerHTML=e}g=db(g,d.childNodes);d=f.firstChild;d.textContent=""}else g.push(b.createTextNode(a));
f.textContent="";f.innerHTML="";r(g,function(a){f.appendChild(a)});return f}function U(a){if(a instanceof U)return a;var b;C(a)&&(a=V(a),b=!0);if(!(this instanceof U)){if(b&&"<"!==a.charAt(0))throw oc("nosel");return new U(a)}if(b){b=z.document;var d;a=(d=tg.exec(a))?[b.createElement(d[1])]:(d=gd(a,b))?d.childNodes:[];pc(this,a)}else B(a)?hd(a):pc(this,a)}function qc(a){return a.cloneNode(!0)}function zb(a,b){!b&&mc(a)&&x.cleanData([a]);a.querySelectorAll&&x.cleanData(a.querySelectorAll("*"))}function id(a){for(var b in a)return!1;
return!0}function jd(a){var b=a.ng339,d=b&&Ka[b],c=d&&d.events,d=d&&d.data;d&&!id(d)||c&&!id(c)||(delete Ka[b],a.ng339=void 0)}function kd(a,b,d,c){if(w(c))throw oc("offargs");var e=(c=Ab(a))&&c.events,f=c&&c.handle;if(f){if(b){var g=function(b){var c=e[b];w(d)&&cb(c||[],d);w(d)&&c&&0<c.length||(a.removeEventListener(b,f),delete e[b])};r(b.split(" "),function(a){g(a);Bb[a]&&g(Bb[a])})}else for(b in e)"$destroy"!==b&&a.removeEventListener(b,f),delete e[b];jd(a)}}function rc(a,b){var d=a.ng339;if(d=
d&&Ka[d])b?delete d.data[b]:d.data={},jd(a)}function Ab(a,b){var d=a.ng339,d=d&&Ka[d];b&&!d&&(a.ng339=d=++ug,d=Ka[d]={events:{},data:{},handle:void 0});return d}function sc(a,b,d){if(mc(a)){var c,e=w(d),f=!e&&b&&!D(b),g=!b;a=(a=Ab(a,!f))&&a.data;if(e)a[yb(b)]=d;else{if(g)return a;if(f)return a&&a[yb(b)];for(c in b)a[yb(c)]=b[c]}}}function Cb(a,b){return a.getAttribute?-1<(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+b+" "):!1}function Db(a,b){if(b&&a.setAttribute){var d=
(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," "),c=d;r(b.split(" "),function(a){a=V(a);c=c.replace(" "+a+" "," ")});c!==d&&a.setAttribute("class",V(c))}}function Eb(a,b){if(b&&a.setAttribute){var d=(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," "),c=d;r(b.split(" "),function(a){a=V(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});c!==d&&a.setAttribute("class",V(c))}}function pc(a,b){if(b)if(b.nodeType)a[a.length++]=b;else{var d=b.length;if("number"===typeof d&&b.window!==b){if(d)for(var c=
0;c<d;c++)a[a.length++]=b[c]}else a[a.length++]=b}}function ld(a,b){return Fb(a,"$"+(b||"ngController")+"Controller")}function Fb(a,b,d){9===a.nodeType&&(a=a.documentElement);for(b=H(b)?b:[b];a;){for(var c=0,e=b.length;c<e;c++)if(w(d=x.data(a,b[c])))return d;a=a.parentNode||11===a.nodeType&&a.host}}function md(a){for(zb(a,!0);a.firstChild;)a.removeChild(a.firstChild)}function Gb(a,b){b||zb(a);var d=a.parentNode;d&&d.removeChild(a)}function vg(a,b){b=b||z;if("complete"===b.document.readyState)b.setTimeout(a);
else x(b).on("load",a)}function hd(a){function b(){z.document.removeEventListener("DOMContentLoaded",b);z.removeEventListener("load",b);a()}"complete"===z.document.readyState?z.setTimeout(a):(z.document.addEventListener("DOMContentLoaded",b),z.addEventListener("load",b))}function nd(a,b){var d=Hb[b.toLowerCase()];return d&&od[ua(a)]&&d}function wg(a,b){var d=function(c,d){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=b[d||c.type],g=f?f.length:0;if(g){if(A(c.immediatePropagationStopped)){var k=
c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=!0;c.stopPropagation&&c.stopPropagation();k&&k.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};var h=f.specialHandlerWrapper||xg;1<g&&(f=ja(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||h(a,c,f[l])}};d.elem=a;return d}function xg(a,b,d){d.call(a,b)}function yg(a,b,d){var c=b.relatedTarget;c&&(c===a||zg.call(a,c))||d.call(a,b)}function ng(){this.$get=
function(){return S(U,{hasClass:function(a,b){a.attr&&(a=a[0]);return Cb(a,b)},addClass:function(a,b){a.attr&&(a=a[0]);return Eb(a,b)},removeClass:function(a,b){a.attr&&(a=a[0]);return Db(a,b)}})}}function La(a,b){var d=a&&a.$$hashKey;if(d)return"function"===typeof d&&(d=a.$$hashKey()),d;d=typeof a;return d="function"===d||"object"===d&&null!==a?a.$$hashKey=d+":"+(b||we)():d+":"+a}function pd(){this._keys=[];this._values=[];this._lastKey=NaN;this._lastIndex=-1}function qd(a){a=Function.prototype.toString.call(a).replace(Ag,
"");return a.match(Bg)||a.match(Cg)}function Dg(a){return(a=qd(a))?"function("+(a[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function fb(a,b){function d(a){return function(b,c){if(D(b))r(b,Zb(a));else return a(b,c)}}function c(a,b){Ja(a,"service");if(B(b)||H(b))b=n.instantiate(b);if(!b.$get)throw Ca("pget",a);return p[a+"Provider"]=b}function e(a,b){return function(){var c=t.invoke(b,this);if(A(c))throw Ca("undef",a);return c}}function f(a,b,d){return c(a,{$get:!1!==d?e(a,b):b})}function g(a){gb(A(a)||
H(a),"modulesToLoad","not an array");var b=[],c;r(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=n.get(e[0]);f[e[1]].apply(f,e[2])}}if(!m.get(a)){m.set(a,!0);try{C(a)?(c=lc(a),t.modules[a]=c,b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):B(a)?b.push(n.invoke(a)):H(a)?b.push(n.invoke(a)):tb(a,"module")}catch(e){throw H(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1===e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Ca("modulerr",
a,e.stack||e.message||e);}}});return b}function k(a,c){function d(b,e){if(a.hasOwnProperty(b)){if(a[b]===h)throw Ca("cdep",b+" <- "+l.join(" <- "));return a[b]}try{return l.unshift(b),a[b]=h,a[b]=c(b,e),a[b]}catch(f){throw a[b]===h&&delete a[b],f;}finally{l.shift()}}function e(a,c,f){var g=[];a=fb.$$annotate(a,b,f);for(var h=0,k=a.length;h<k;h++){var l=a[h];if("string"!==typeof l)throw Ca("itkn",l);g.push(c&&c.hasOwnProperty(l)?c[l]:d(l,f))}return g}return{invoke:function(a,b,c,d){"string"===typeof c&&
(d=c,c=null);c=e(a,c,d);H(a)&&(a=a[a.length-1]);d=a;if(wa||"function"!==typeof d)d=!1;else{var f=d.$$ngIsClass;Ga(f)||(f=d.$$ngIsClass=/^class\b/.test(Function.prototype.toString.call(d)));d=f}return d?(c.unshift(null),new (Function.prototype.bind.apply(a,c))):a.apply(b,c)},instantiate:function(a,b,c){var d=H(a)?a[a.length-1]:a;a=e(a,b,c);a.unshift(null);return new (Function.prototype.bind.apply(d,a))},get:d,annotate:fb.$$annotate,has:function(b){return p.hasOwnProperty(b+"Provider")||a.hasOwnProperty(b)}}}
b=!0===b;var h={},l=[],m=new Ib,p={$provide:{provider:d(c),factory:d(f),service:d(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:d(function(a,b){return f(a,ia(b),!1)}),constant:d(function(a,b){Ja(a,"constant");p[a]=b;s[a]=b}),decorator:function(a,b){var c=n.get(a+"Provider"),d=c.$get;c.$get=function(){var a=t.invoke(d,c);return t.invoke(b,null,{$delegate:a})}}}},n=p.$injector=k(p,function(a,b){ca.isString(b)&&l.push(b);throw Ca("unpr",l.join(" <- "));}),s={},
G=k(s,function(a,b){var c=n.get(a+"Provider",b);return t.invoke(c.$get,c,void 0,a)}),t=G;p.$injectorProvider={$get:ia(G)};t.modules=n.modules=T();var N=g(a),t=G.get("$injector");t.strictDi=b;r(N,function(a){a&&t.invoke(a)});t.loadNewModules=function(a){r(g(a),function(a){a&&t.invoke(a)})};return t}function Bf(){var a=!0;this.disableAutoScrolling=function(){a=!1};this.$get=["$window","$location","$rootScope",function(b,d,c){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===
ua(a))return b=a,!0});return b}function f(a){if(a){a.scrollIntoView();var c;c=g.yOffset;B(c)?c=c():ac(c)?(c=c[0],c="fixed"!==b.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):X(c)||(c=0);c&&(a=a.getBoundingClientRect().top,b.scrollBy(0,a-c))}else b.scrollTo(0,0)}function g(a){a=C(a)?a:X(a)?a.toString():d.hash();var b;a?(b=k.getElementById(a))?f(b):(b=e(k.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var k=b.document;a&&c.$watch(function(){return d.hash()},function(a,b){a===
b&&""===a||vg(function(){c.$evalAsync(g)})});return g}]}function ib(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;H(a)&&(a=a.join(" "));H(b)&&(b=b.join(" "));return a+" "+b}function Eg(a){C(a)&&(a=a.split(" "));var b=T();r(a,function(a){a.length&&(b[a]=!0)});return b}function ra(a){return D(a)?a:{}}function Fg(a,b,d,c,e){function f(){pa=null;k()}function g(){t=y();t=A(t)?null:t;va(t,P)&&(t=P);N=P=t}function k(){var a=N;g();if(v!==h.url()||a!==t)v=h.url(),N=t,r(J,function(a){a(h.url(),t)})}
var h=this,l=a.location,m=a.history,p=a.setTimeout,n=a.clearTimeout,s={},G=e(d);h.isMock=!1;h.$$completeOutstandingRequest=G.completeTask;h.$$incOutstandingRequestCount=G.incTaskCount;h.notifyWhenNoOutstandingRequests=G.notifyWhenNoPendingTasks;var t,N,v=l.href,kc=b.find("base"),pa=null,y=c.history?function(){try{return m.state}catch(a){}}:E;g();h.url=function(b,d,e){A(e)&&(e=null);l!==a.location&&(l=a.location);m!==a.history&&(m=a.history);if(b){var f=N===e;b=ga(b).href;if(v===b&&(!c.history||f))return h;
var k=v&&Da(v)===Da(b);v=b;N=e;!c.history||k&&f?(k||(pa=b),d?l.replace(b):k?(d=l,e=b,f=e.indexOf("#"),e=-1===f?"":e.substr(f),d.hash=e):l.href=b,l.href!==b&&(pa=b)):(m[d?"replaceState":"pushState"](e,"",b),g());pa&&(pa=b);return h}return(pa||l.href).replace(/#$/,"")};h.state=function(){return t};var J=[],I=!1,P=null;h.onUrlChange=function(b){if(!I){if(c.history)x(a).on("popstate",f);x(a).on("hashchange",f);I=!0}J.push(b);return b};h.$$applicationDestroyed=function(){x(a).off("hashchange popstate",
f)};h.$$checkUrlChange=k;h.baseHref=function(){var a=kc.attr("href");return a?a.replace(/^(https?:)?\/\/[^/]*/,""):""};h.defer=function(a,b,c){var d;b=b||0;c=c||G.DEFAULT_TASK_TYPE;G.incTaskCount(c);d=p(function(){delete s[d];G.completeTask(a,c)},b);s[d]=c;return d};h.defer.cancel=function(a){if(s.hasOwnProperty(a)){var b=s[a];delete s[a];n(a);G.completeTask(E,b);return!0}return!1}}function If(){this.$get=["$window","$log","$sniffer","$document","$$taskTrackerFactory",function(a,b,d,c,e){return new Fg(a,
c,b,d,e)}]}function Jf(){this.$get=function(){function a(a,c){function e(a){a!==p&&(n?n===a&&(n=a.n):n=a,f(a.n,a.p),f(a,p),p=a,p.n=null)}function f(a,b){a!==b&&(a&&(a.p=b),b&&(b.n=a))}if(a in b)throw F("$cacheFactory")("iid",a);var g=0,k=S({},c,{id:a}),h=T(),l=c&&c.capacity||Number.MAX_VALUE,m=T(),p=null,n=null;return b[a]={put:function(a,b){if(!A(b)){if(l<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}a in h||g++;h[a]=b;g>l&&this.remove(n.key);return b}},get:function(a){if(l<Number.MAX_VALUE){var b=
m[a];if(!b)return;e(b)}return h[a]},remove:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;b===p&&(p=b.p);b===n&&(n=b.n);f(b.n,b.p);delete m[a]}a in h&&(delete h[a],g--)},removeAll:function(){h=T();g=0;m=T();p=n=null},destroy:function(){m=k=h=null;delete b[a]},info:function(){return S({},k,{size:g})}}}var b={};a.info=function(){var a={};r(b,function(b,e){a[e]=b.info()});return a};a.get=function(a){return b[a]};return a}}function hg(){this.$get=["$cacheFactory",function(a){return a("templates")}]}
function Zc(a,b){function d(a,b,c){var d=/^([@&]|[=<](\*?))(\??)\s*([\w$]*)$/,e=T();r(a,function(a,f){a=a.trim();if(a in p)e[f]=p[a];else{var g=a.match(d);if(!g)throw $("iscp",b,f,a,c?"controller bindings definition":"isolate scope definition");e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f};g[4]&&(p[a]=e[f])}});return e}function c(a){var b=a.charAt(0);if(!b||b!==K(b))throw $("baddir",a);if(a!==a.trim())throw $("baddir",a);}function e(a){var b=a.require||a.controller&&
a.name;!H(b)&&D(b)&&r(b,function(a,c){var d=a.match(l);a.substring(d[0].length)||(b[c]=d[0]+c)});return b}var f={},g=/^\s*directive:\s*([\w-]+)\s+(.*)$/,k=/(([\w-]+)(?::([^;]+))?;?)/,h=Ae("ngSrc,ngSrcset,src,srcset"),l=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,m=/^(on[a-z]+|formaction)$/,p=T();this.directive=function pa(b,d){gb(b,"name");Ja(b,"directive");C(b)?(c(b),gb(d,"directiveFactory"),f.hasOwnProperty(b)||(f[b]=[],a.factory(b+"Directive",["$injector","$exceptionHandler",function(a,c){var d=[];r(f[b],function(f,
g){try{var h=a.invoke(f);B(h)?h={compile:ia(h)}:!h.compile&&h.link&&(h.compile=ia(h.link));h.priority=h.priority||0;h.index=g;h.name=h.name||b;h.require=e(h);var k=h,l=h.restrict;if(l&&(!C(l)||!/[EACM]/.test(l)))throw $("badrestrict",l,b);k.restrict=l||"EA";h.$$moduleName=f.$$moduleName;d.push(h)}catch(m){c(m)}});return d}])),f[b].push(d)):r(b,Zb(pa));return this};this.component=function y(a,b){function c(a){function e(b){return B(b)||H(b)?function(c,d){return a.invoke(b,this,{$element:c,$attrs:d})}:
b}var f=b.template||b.templateUrl?b.template:"",g={controller:d,controllerAs:Gg(b.controller)||b.controllerAs||"$ctrl",template:e(f),templateUrl:e(b.templateUrl),transclude:b.transclude,scope:{},bindToController:b.bindings||{},restrict:"E",require:b.require};r(b,function(a,b){"$"===b.charAt(0)&&(g[b]=a)});return g}if(!C(a))return r(a,Zb(Va(this,y))),this;var d=b.controller||function(){};r(b,function(a,b){"$"===b.charAt(0)&&(c[b]=a,B(d)&&(d[b]=a))});c.$inject=["$injector"];return this.directive(a,
c)};this.aHrefSanitizationTrustedUrlList=function(a){return w(a)?(b.aHrefSanitizationTrustedUrlList(a),this):b.aHrefSanitizationTrustedUrlList()};Object.defineProperty(this,"aHrefSanitizationWhitelist",{get:function(){return this.aHrefSanitizationTrustedUrlList},set:function(a){this.aHrefSanitizationTrustedUrlList=a}});this.imgSrcSanitizationTrustedUrlList=function(a){return w(a)?(b.imgSrcSanitizationTrustedUrlList(a),this):b.imgSrcSanitizationTrustedUrlList()};Object.defineProperty(this,"imgSrcSanitizationWhitelist",
{get:function(){return this.imgSrcSanitizationTrustedUrlList},set:function(a){this.imgSrcSanitizationTrustedUrlList=a}});var n=!0;this.debugInfoEnabled=function(a){return w(a)?(n=a,this):n};var s=!1;this.strictComponentBindingsEnabled=function(a){return w(a)?(s=a,this):s};var G=10;this.onChangesTtl=function(a){return arguments.length?(G=a,this):G};var t=!0;this.commentDirectivesEnabled=function(a){return arguments.length?(t=a,this):t};var N=!0;this.cssClassDirectivesEnabled=function(a){return arguments.length?
(N=a,this):N};var v=T();this.addPropertySecurityContext=function(a,b,c){var d=a.toLowerCase()+"|"+b.toLowerCase();if(d in v&&v[d]!==c)throw $("ctxoverride",a,b,v[d],c);v[d]=c;return this};(function(){function a(b,c){r(c,function(a){v[a.toLowerCase()]=b})}a(W.HTML,["iframe|srcdoc","*|innerHTML","*|outerHTML"]);a(W.CSS,["*|style"]);a(W.URL,"area|href area|ping a|href a|ping blockquote|cite body|background del|cite input|src ins|cite q|cite".split(" "));a(W.MEDIA_URL,"audio|src img|src img|srcset source|src source|srcset track|src video|src video|poster".split(" "));
a(W.RESOURCE_URL,"*|formAction applet|code applet|codebase base|href embed|src frame|src form|action head|profile html|manifest iframe|src link|href media|src object|codebase object|data script|src".split(" "))})();this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$sce","$animate",function(a,b,c,e,p,M,L,u,R){function q(){try{if(!--Ja)throw Ua=void 0,$("infchng",G);L.$apply(function(){for(var a=0,b=Ua.length;a<b;++a)try{Ua[a]()}catch(d){c(d)}Ua=
void 0})}finally{Ja++}}function ma(a,b){if(!a)return a;if(!C(a))throw $("srcset",b,a.toString());for(var c="",d=V(a),e=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,e=/\s/.test(d)?e:/(,)/,d=d.split(e),e=Math.floor(d.length/2),f=0;f<e;f++)var g=2*f,c=c+u.getTrustedMediaUrl(V(d[g])),c=c+(" "+V(d[g+1]));d=V(d[2*f]).split(/\s/);c+=u.getTrustedMediaUrl(V(d[0]));2===d.length&&(c+=" "+V(d[1]));return c}function w(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr=
{};this.$$element=a}function O(a,b,c){Fa.innerHTML="<span "+b+">";b=Fa.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function sa(a,b){try{a.addClass(b)}catch(c){}}function da(a,b,c,d,e){a instanceof x||(a=x(a));var f=Xa(a,b,a,c,d,e);da.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw $("multilink");gb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;
h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ua(d)&&la.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?x(ja(g,x("<div></div>").append(a).html())):c?Wa.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);da.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Xa(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,p,I,t;if(n)for(t=Array(c.length),m=0;m<h.length;m+=3)f=h[m],t[f]=c[f];else t=c;m=0;for(p=h.length;m<
p;)k=t[h[m++]],c=h[m++],f=h[m++],c?(c.scope?(l=a.$new(),da.$$addScopeInfo(x(k),l)):l=a,I=c.transcludeOnThisElement?ka(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?ka(a,b):null,c(f,l,k,d,I)):f&&f(a,k.childNodes,void 0,e)}for(var h=[],k=H(a)||a instanceof x,l,m,p,I,n,t=0;t<a.length;t++){l=new w;11===wa&&jb(a,t,k);m=tc(a[t],[],l,0===t?d:void 0,e);(f=m.length?aa(m,a[t],l,b,c,null,[],[],f):null)&&f.scope&&da.$$addScopeClass(l.$$element);l=f&&f.terminal||!(p=a[t].childNodes)||!p.length?null:Xa(p,
f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||l)h.push(t,f,l),I=!0,n=n||f;f=null}return I?g:null}function jb(a,b,c){var d=a[b],e=d.parentNode,f;if(d.nodeType===Pa)for(;;){f=e?d.nextSibling:a[b+1];if(!f||f.nodeType!==Pa)break;d.nodeValue+=f.nodeValue;f.parentNode&&f.parentNode.removeChild(f);c&&f===a[b+1]&&a.splice(b+1,1)}}function ka(a,b,c){function d(e,f,g,h,k){e||(e=a.$new(!1,k),e.$$transcluded=!0);return b(e,f,{parentBoundTranscludeFn:c,transcludeControllers:g,
futureParentElement:h})}var e=d.$$slots=T(),f;for(f in b.$$slots)e[f]=b.$$slots[f]?ka(a,b.$$slots[f],c):null;return d}function tc(a,b,d,e,f){var g=d.$attr,h;switch(a.nodeType){case 1:h=ua(a);Y(b,xa(h),"E",e,f);for(var l,m,n,t,J,s=a.attributes,v=0,G=s&&s.length;v<G;v++){var P=!1,N=!1,r=!1,y=!1,u=!1,M;l=s[v];m=l.name;t=l.value;n=xa(m.toLowerCase());(J=n.match(Ra))?(r="Attr"===J[1],y="Prop"===J[1],u="On"===J[1],m=m.replace(rd,"").toLowerCase().substr(4+J[1].length).replace(/_(.)/g,function(a,b){return b.toUpperCase()})):
(M=n.match(Sa))&&ca(M[1])&&(P=m,N=m.substr(0,m.length-5)+"end",m=m.substr(0,m.length-6));if(y||u)d[n]=t,g[n]=l.name,y?Ea(a,b,n,m):b.push(sd(p,L,c,n,m,!1));else{n=xa(m.toLowerCase());g[n]=m;if(r||!d.hasOwnProperty(n))d[n]=t,nd(a,n)&&(d[n]=!0);Ia(a,b,t,n,r);Y(b,n,"A",e,f,P,N)}}"input"===h&&"hidden"===a.getAttribute("type")&&a.setAttribute("autocomplete","off");if(!Qa)break;g=a.className;D(g)&&(g=g.animVal);if(C(g)&&""!==g)for(;a=k.exec(g);)n=xa(a[2]),Y(b,n,"C",e,f)&&(d[n]=V(a[3])),g=g.substr(a.index+
a[0].length);break;case Pa:na(b,a.nodeValue);break;case 8:if(!Oa)break;F(a,b,d,e,f)}b.sort(ia);return b}function F(a,b,c,d,e){try{var f=g.exec(a.nodeValue);if(f){var h=xa(f[1]);Y(b,h,"M",d,e)&&(c[h]=V(f[2]))}}catch(k){}}function U(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw $("uterdir",b,c);1===a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return x(d)}function W(a,b,c){return function(d,e,f,g,h){e=U(e[0],
b,c);return a(d,e,f,g,h)}}function Z(a,b,c,d,e,f){var g;return a?da(b,c,d,e,f):function(){g||(g=da(b,c,d,e,f),b=c=f=null);return g.apply(this,arguments)}}function aa(a,b,d,e,f,g,h,k,l){function m(a,b,c,d){if(a){c&&(a=W(a,c,d));a.require=u.require;a.directiveName=Q;if(s===u||u.$$isolateScope)a=Ba(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=W(b,c,d));b.require=u.require;b.directiveName=Q;if(s===u||u.$$isolateScope)b=Ba(b,{isolateScope:!0});k.push(b)}}function p(a,e,f,g,l){function m(a,b,c,d){var e;bb(a)||
(d=c,c=b,b=a,a=void 0);N&&(e=P);c||(c=N?Q.parent():Q);if(d){var f=l.$$slots[d];if(f)return f(a,b,e,c,R);if(A(f))throw $("noslot",d,Aa(Q));}else return l(a,b,e,c,R)}var n,u,L,y,G,P,M,Q;b===f?(g=d,Q=d.$$element):(Q=x(f),g=new w(Q,d));G=e;s?y=e.$new(!0):t&&(G=e.$parent);l&&(M=m,M.$$boundTransclude=l,M.isSlotFilled=function(a){return!!l.$$slots[a]});J&&(P=ea(Q,g,M,J,y,e,s));s&&(da.$$addScopeInfo(Q,y,!0,!(v&&(v===s||v===s.$$originalDirective))),da.$$addScopeClass(Q,!0),y.$$isolateBindings=s.$$isolateBindings,
u=Da(e,g,y,y.$$isolateBindings,s),u.removeWatches&&y.$on("$destroy",u.removeWatches));for(n in P){u=J[n];L=P[n];var Hg=u.$$bindings.bindToController;L.instance=L();Q.data("$"+u.name+"Controller",L.instance);L.bindingInfo=Da(G,g,L.instance,Hg,u)}r(J,function(a,b){var c=a.require;a.bindToController&&!H(c)&&D(c)&&S(P[b].instance,X(b,c,Q,P))});r(P,function(a){var b=a.instance;if(B(b.$onChanges))try{b.$onChanges(a.bindingInfo.initialChanges)}catch(d){c(d)}if(B(b.$onInit))try{b.$onInit()}catch(e){c(e)}B(b.$doCheck)&&
(G.$watch(function(){b.$doCheck()}),b.$doCheck());B(b.$onDestroy)&&G.$on("$destroy",function(){b.$onDestroy()})});n=0;for(u=h.length;n<u;n++)L=h[n],Ca(L,L.isolateScope?y:e,Q,g,L.require&&X(L.directiveName,L.require,Q,P),M);var R=e;s&&(s.template||null===s.templateUrl)&&(R=y);a&&a(R,f.childNodes,void 0,l);for(n=k.length-1;0<=n;n--)L=k[n],Ca(L,L.isolateScope?y:e,Q,g,L.require&&X(L.directiveName,L.require,Q,P),M);r(P,function(a){a=a.instance;B(a.$postLink)&&a.$postLink()})}l=l||{};for(var n=-Number.MAX_VALUE,
t=l.newScopeDirective,J=l.controllerDirectives,s=l.newIsolateScopeDirective,v=l.templateDirective,L=l.nonTlbTranscludeDirective,G=!1,P=!1,N=l.hasElementTranscludeDirective,y=d.$$element=x(b),u,Q,M,R=e,q,ma=!1,Jb=!1,O,sa=0,C=a.length;sa<C;sa++){u=a[sa];var E=u.$$start,jb=u.$$end;E&&(y=U(b,E,jb));M=void 0;if(n>u.priority)break;if(O=u.scope)u.templateUrl||(D(O)?(ba("new/isolated scope",s||t,u,y),s=u):ba("new/isolated scope",s,u,y)),t=t||u;Q=u.name;if(!ma&&(u.replace&&(u.templateUrl||u.template)||u.transclude&&
!u.$$tlb)){for(O=sa+1;ma=a[O++];)if(ma.transclude&&!ma.$$tlb||ma.replace&&(ma.templateUrl||ma.template)){Jb=!0;break}ma=!0}!u.templateUrl&&u.controller&&(J=J||T(),ba("'"+Q+"' controller",J[Q],u,y),J[Q]=u);if(O=u.transclude)if(G=!0,u.$$tlb||(ba("transclusion",L,u,y),L=u),"element"===O)N=!0,n=u.priority,M=y,y=d.$$element=x(da.$$createComment(Q,d[Q])),b=y[0],oa(f,Ha.call(M,0),b),R=Z(Jb,M,e,n,g&&g.name,{nonTlbTranscludeDirective:L});else{var ka=T();if(D(O)){M=z.document.createDocumentFragment();var Xa=
T(),F=T();r(O,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Xa[a]=b;ka[b]=null;F[b]=c});r(y.contents(),function(a){var b=Xa[xa(ua(a))];b?(F[b]=!0,ka[b]=ka[b]||z.document.createDocumentFragment(),ka[b].appendChild(a)):M.appendChild(a)});r(F,function(a,b){if(!a)throw $("reqslot",b);});for(var K in ka)ka[K]&&(R=x(ka[K].childNodes),ka[K]=Z(Jb,R,e));M=x(M.childNodes)}else M=x(qc(b)).contents();y.empty();R=Z(Jb,M,e,void 0,void 0,{needsNewScope:u.$$isolateScope||u.$$newScope});R.$$slots=ka}if(u.template)if(P=
!0,ba("template",v,u,y),v=u,O=B(u.template)?u.template(y,d):u.template,O=Na(O),u.replace){g=u;M=nc.test(O)?td(ja(u.templateNamespace,V(O))):[];b=M[0];if(1!==M.length||1!==b.nodeType)throw $("tplrt",Q,"");oa(f,y,b);C={$attr:{}};O=tc(b,[],C);var Ig=a.splice(sa+1,a.length-(sa+1));(s||t)&&fa(O,s,t);a=a.concat(O).concat(Ig);ga(d,C);C=a.length}else y.html(O);if(u.templateUrl)P=!0,ba("template",v,u,y),v=u,u.replace&&(g=u),p=ha(a.splice(sa,a.length-sa),y,d,f,G&&R,h,k,{controllerDirectives:J,newScopeDirective:t!==
u&&t,newIsolateScopeDirective:s,templateDirective:v,nonTlbTranscludeDirective:L}),C=a.length;else if(u.compile)try{q=u.compile(y,d,R);var Y=u.$$originalDirective||u;B(q)?m(null,Va(Y,q),E,jb):q&&m(Va(Y,q.pre),Va(Y,q.post),E,jb)}catch(ca){c(ca,Aa(y))}u.terminal&&(p.terminal=!0,n=Math.max(n,u.priority))}p.scope=t&&!0===t.scope;p.transcludeOnThisElement=G;p.templateOnThisElement=P;p.transclude=R;l.hasElementTranscludeDirective=N;return p}function X(a,b,c,d){var e;if(C(b)){var f=b.match(l);b=b.substring(f[0].length);
var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e="^^"===g&&c[0]&&9===c[0].nodeType?null:g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw $("ctreq",b,a);}else if(H(b))for(e=[],g=0,f=b.length;g<f;g++)e[g]=X(a,b[g],c,d);else D(b)&&(e={},r(b,function(b,f){e[f]=X(a,b,c,d)}));return e||null}function ea(a,b,c,d,e,f,g){var h=T(),k;for(k in d){var l=d[k],m={$scope:l===g||l.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},p=l.controller;"@"===
p&&(p=b[l.name]);m=M(p,m,!0,l.controllerAs);h[l.name]=m;a.data("$"+l.name+"Controller",m.instance)}return h}function fa(a,b,c){for(var d=0,e=a.length;d<e;d++)a[d]=bc(a[d],{$$isolateScope:b,$$newScope:c})}function Y(b,c,e,g,h,k,l){if(c===h)return null;var m=null;if(f.hasOwnProperty(c)){h=a.get(c+"Directive");for(var p=0,n=h.length;p<n;p++)if(c=h[p],(A(g)||g>c.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=bc(c,{$$start:k,$$end:l}));if(!c.$$bindings){var I=m=c,t=c.name,u={isolateScope:null,bindToController:null};
D(I.scope)&&(!0===I.bindToController?(u.bindToController=d(I.scope,t,!0),u.isolateScope={}):u.isolateScope=d(I.scope,t,!1));D(I.bindToController)&&(u.bindToController=d(I.bindToController,t,!0));if(u.bindToController&&!I.controller)throw $("noctrl",t);m=m.$$bindings=u;D(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function ca(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d<e;d++)if(b=c[d],b.multiElement)return!0;return!1}function ga(a,b){var c=
b.$attr,d=a.$attr;r(a,function(d,e){"$"!==e.charAt(0)&&(b[e]&&b[e]!==d&&(d=d.length?d+(("style"===e?";":" ")+b[e]):b[e]),a.$set(e,d,!0,c[e]))});r(b,function(b,e){a.hasOwnProperty(e)||"$"===e.charAt(0)||(a[e]=b,"class"!==e&&"style"!==e&&(d[e]=c[e]))})}function ha(a,b,d,f,g,h,k,l){var m=[],p,n,t=b[0],u=a.shift(),J=bc(u,{templateUrl:null,transclude:null,replace:null,$$originalDirective:u}),s=B(u.templateUrl)?u.templateUrl(b,d):u.templateUrl,L=u.templateNamespace;b.empty();e(s).then(function(c){var e,
I;c=Na(c);if(u.replace){c=nc.test(c)?td(ja(L,V(c))):[];e=c[0];if(1!==c.length||1!==e.nodeType)throw $("tplrt",u.name,s);c={$attr:{}};oa(f,b,e);var v=tc(e,[],c);D(u.scope)&&fa(v,!0);a=v.concat(a);ga(d,c)}else e=t,b.html(c);a.unshift(J);p=aa(a,e,d,g,b,u,h,k,l);r(f,function(a,c){a===e&&(f[c]=b[0])});for(n=Xa(b[0].childNodes,g);m.length;){c=m.shift();I=m.shift();var y=m.shift(),P=m.shift(),v=b[0];if(!c.$$destroyed){if(I!==t){var G=I.className;l.hasElementTranscludeDirective&&u.replace||(v=qc(e));oa(y,
x(I),v);sa(x(v),G)}I=p.transcludeOnThisElement?ka(c,p.transclude,P):P;p(n,c,v,f,I)}}m=null}).catch(function(a){dc(a)&&c(a)});return function(a,b,c,d,e){a=e;b.$$destroyed||(m?m.push(b,c,d,a):(p.transcludeOnThisElement&&(a=ka(b,p.transclude,e)),p(n,b,c,d,a)))}}function ia(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function ba(a,b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw $("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),
a,Aa(d));}function na(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&da.$$addBindingClass(a);return function(a,c){var e=c.parent();b||da.$$addBindingClass(e);da.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function ja(a,b){a=K(a||"html");switch(a){case "svg":case "math":var c=z.document.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function qa(a,b){if("srcdoc"===
b)return u.HTML;if("src"===b||"ngSrc"===b)return-1===["img","video","audio","source","track"].indexOf(a)?u.RESOURCE_URL:u.MEDIA_URL;if("xlinkHref"===b)return"image"===a?u.MEDIA_URL:"a"===a?u.URL:u.RESOURCE_URL;if("form"===a&&"action"===b||"base"===a&&"href"===b||"link"===a&&"href"===b)return u.RESOURCE_URL;if("a"===a&&("href"===b||"ngHref"===b))return u.URL}function ya(a,b){var c=b.toLowerCase();return v[a+"|"+c]||v["*|"+c]}function za(a){return ma(u.valueOf(a),"ng-prop-srcset")}function Ea(a,b,c,
d){if(m.test(d))throw $("nodomevents");a=ua(a);var e=ya(a,d),f=Ta;"srcset"!==d||"img"!==a&&"source"!==a?e&&(f=u.getTrusted.bind(u,e)):f=za;b.push({priority:100,compile:function(a,b){var e=p(b[c]),g=p(b[c],function(a){return u.valueOf(a)});return{pre:function(a,b){function c(){var g=e(a);b[0][d]=f(g)}c();a.$watch(g,c)}}}})}function Ia(a,c,d,e,f){var g=ua(a),k=qa(g,e),l=h[e]||f,p=b(d,!f,k,l);if(p){if("multiple"===e&&"select"===g)throw $("selmulti",Aa(a));if(m.test(e))throw $("nodomevents");c.push({priority:100,
compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=T());var g=f[e];g!==d&&(p=g&&b(g,!0,k,l),d=g);p&&(f[e]=p(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(p,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function oa(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]===d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&
(a.context=c);break}f&&f.replaceChild(c,d);a=z.document.createDocumentFragment();for(g=0;g<e;g++)a.appendChild(b[g]);x.hasData(d)&&(x.data(c,x.data(d)),x(d).off("$destroy"));x.cleanData(a.querySelectorAll("*"));for(g=1;g<e;g++)delete b[g];b[0]=c;b.length=1}function Ba(a,b){return S(function(){return a.apply(null,arguments)},a,b)}function Ca(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,Aa(d))}}function ra(a,b){if(s)throw $("missingattr",a,b);}function Da(a,c,d,e,f){function g(b,c,e){B(d.$onChanges)&&
!ec(c,e)&&(Ua||(a.$$postDigest(q),Ua=[]),m||(m={},Ua.push(h)),m[b]&&(e=m[b].previousValue),m[b]=new Kb(e,c))}function h(){d.$onChanges(m);m=void 0}var k=[],l={},m;r(e,function(e,h){var m=e.attrName,n=e.optional,I,t,u,s;switch(e.mode){case "@":n||ta.call(c,m)||(ra(m,f.name),d[h]=c[m]=void 0);n=c.$observe(m,function(a){if(C(a)||Ga(a))g(h,a,d[h]),d[h]=a});c.$$observers[m].$$scope=a;I=c[m];C(I)?d[h]=b(I)(a):Ga(I)&&(d[h]=I);l[h]=new Kb(uc,d[h]);k.push(n);break;case "=":if(!ta.call(c,m)){if(n)break;ra(m,
f.name);c[m]=void 0}if(n&&!c[m])break;t=p(c[m]);s=t.literal?va:ec;u=t.assign||function(){I=d[h]=t(a);throw $("nonassign",c[m],m,f.name);};I=d[h]=t(a);n=function(b){s(b,d[h])||(s(b,I)?u(a,b=d[h]):d[h]=b);return I=b};n.$stateful=!0;n=e.collection?a.$watchCollection(c[m],n):a.$watch(p(c[m],n),null,t.literal);k.push(n);break;case "<":if(!ta.call(c,m)){if(n)break;ra(m,f.name);c[m]=void 0}if(n&&!c[m])break;t=p(c[m]);var v=t.literal,L=d[h]=t(a);l[h]=new Kb(uc,d[h]);n=a[e.collection?"$watchCollection":"$watch"](t,
function(a,b){if(b===a){if(b===L||v&&va(b,L))return;b=L}g(h,a,b);d[h]=a});k.push(n);break;case "&":n||ta.call(c,m)||ra(m,f.name);t=c.hasOwnProperty(m)?p(c[m]):E;if(t===E&&n)break;d[h]=function(b){return t(a,b)}}});return{initialChanges:l,removeWatches:k.length&&function(){for(var a=0,b=k.length;a<b;++a)k[a]()}}}var Ma=/^\w/,Fa=z.document.createElement("div"),Oa=t,Qa=N,Ja=G,Ua;w.prototype={$normalize:xa,$addClass:function(a){a&&0<a.length&&R.addClass(this.$$element,a)},$removeClass:function(a){a&&
0<a.length&&R.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=ud(a,b);c&&c.length&&R.addClass(this.$$element,c);(c=ud(b,a))&&c.length&&R.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=nd(this.$$element[0],a),g=vd[a],h=a;f?(this.$$element.prop(a,b),e=f):g&&(this[g]=b,h=g);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=Xc(a,"-"));"img"===ua(this.$$element)&&"srcset"===a&&(this[a]=b=ma(b,"$set('srcset', value)"));!1!==d&&(null===b||A(b)?this.$$element.removeAttr(e):
Ma.test(e)?f&&!1===b?this.$$element.removeAttr(e):this.$$element.attr(e,b):O(this.$$element[0],e,b));(a=this.$$observers)&&r(a[h],function(a){try{a(b)}catch(d){c(d)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=T()),e=d[a]||(d[a]=[]);e.push(b);L.$evalAsync(function(){e.$$inter||!c.hasOwnProperty(a)||A(c[a])||b(c[a])});return function(){cb(e,b)}}};var Ka=b.startSymbol(),La=b.endSymbol(),Na="{{"===Ka&&"}}"===La?Ta:function(a){return a.replace(/\{\{/g,Ka).replace(/}}/g,La)},Ra=
/^ng(Attr|Prop|On)([A-Z].*)$/,Sa=/^(.+)Start$/;da.$$addBindingInfo=n?function(a,b){var c=a.data("$binding")||[];H(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:E;da.$$addBindingClass=n?function(a){sa(a,"ng-binding")}:E;da.$$addScopeInfo=n?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:E;da.$$addScopeClass=n?function(a,b){sa(a,b?"ng-isolate-scope":"ng-scope")}:E;da.$$createComment=function(a,b){var c="";n&&(c=" "+(a||"")+": ",b&&(c+=b+" "));return z.document.createComment(c)};
return da}]}function Kb(a,b){this.previousValue=a;this.currentValue=b}function xa(a){return a.replace(rd,"").replace(Jg,function(a,d,c){return c?d.toUpperCase():d})}function ud(a,b){var d="",c=a.split(/\s+/),e=b.split(/\s+/),f=0;a:for(;f<c.length;f++){for(var g=c[f],k=0;k<e.length;k++)if(g===e[k])continue a;d+=(0<d.length?" ":"")+g}return d}function td(a){a=x(a);var b=a.length;if(1>=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Pa&&""===d.nodeValue.trim())&&Kg.call(a,b,1)}return a}
function Gg(a,b){if(b&&C(b))return b;if(C(a)){var d=wd.exec(a);if(d)return d[3]}}function Kf(){var a={};this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,d){Ja(b,"controller");D(b)?S(a,b):a[b]=d};this.$get=["$injector",function(b){function d(a,b,d,g){if(!a||!D(a.$scope))throw F("$controller")("noscp",g,b);a.$scope[b]=d}return function(c,e,f,g){var k,h,l;f=!0===f;g&&C(g)&&(l=g);if(C(c)){g=c.match(wd);if(!g)throw xd("ctrlfmt",c);h=g[1];l=l||g[3];c=a.hasOwnProperty(h)?a[h]:Le(e.$scope,
h,!0);if(!c)throw xd("ctrlreg",h);tb(c,h,!0)}if(f)return f=(H(c)?c[c.length-1]:c).prototype,k=Object.create(f||null),l&&d(e,l,k,h||c.name),S(function(){var a=b.invoke(c,k,e,h);a!==k&&(D(a)||B(a))&&(k=a,l&&d(e,l,k,h||c.name));return k},{instance:k,identifier:l});k=b.instantiate(c,e,h);l&&d(e,l,k,h||c.name);return k}}]}function Lf(){this.$get=["$window",function(a){return x(a.document)}]}function Mf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden;
a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function Nf(){this.$get=["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function vc(a){return D(a)?ha(a)?a.toISOString():eb(a):a}function Tf(){this.$get=function(){return function(a){if(!a)return"";var b=[];Qc(a,function(a,c){null===a||A(a)||B(a)||(H(a)?r(a,function(a){b.push(ba(c)+"="+ba(vc(a)))}):b.push(ba(c)+"="+ba(vc(a))))});return b.join("&")}}}function Uf(){this.$get=
function(){return function(a){function b(a,e,f){H(a)?r(a,function(a,c){b(a,e+"["+(D(a)?c:"")+"]")}):D(a)&&!ha(a)?Qc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):(B(a)&&(a=a()),d.push(ba(e)+"="+(null==a?"":ba(vc(a)))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function wc(a,b){if(C(a)){var d=a.replace(Lg,"").trim();if(d){var c=b("Content-Type"),c=c&&0===c.indexOf(yd),e;(e=c)||(e=(e=d.match(Mg))&&Ng[e[0]].test(d));if(e)try{a=Tc(d)}catch(f){if(!c)return a;throw Lb("baddata",a,f);}}}return a}
function zd(a){var b=T(),d;C(a)?r(a.split("\n"),function(a){d=a.indexOf(":");var e=K(V(a.substr(0,d)));a=V(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):D(a)&&r(a,function(a,d){var f=K(d),g=V(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function Ad(a){var b;return function(d){b||(b=zd(a));return d?(d=b[K(d)],void 0===d&&(d=null),d):b}}function Bd(a,b,d,c){if(B(c))return c(a,b,d);r(c,function(c){a=c(a,b,d)});return a}function Sf(){var a=this.defaults={transformResponse:[wc],transformRequest:[function(a){return D(a)&&
"[object File]"!==la.call(a)&&"[object Blob]"!==la.call(a)&&"[object FormData]"!==la.call(a)?eb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ja(xc),put:ja(xc),patch:ja(xc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return w(a)?(b=!!a,this):b};var d=this.interceptors=[],c=this.xsrfTrustedOrigins=[];Object.defineProperty(this,"xsrfWhitelistedOrigins",
{get:function(){return this.xsrfTrustedOrigins},set:function(a){this.xsrfTrustedOrigins=a}});this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(e,f,g,k,h,l,m,p){function n(b){function c(a,b){for(var d=0,e=b.length;d<e;){var f=b[d++],g=b[d++];a=a.then(f,g)}b.length=0;return a}function d(a,b){var c,e={};r(a,function(a,d){B(a)?(c=a(b),null!=c&&(e[d]=c)):e[d]=a});return e}function f(a){var b=S({},a);b.data=Bd(a.data,a.headers,a.status,g.transformResponse);
a=a.status;return 200<=a&&300>a?b:l.reject(b)}if(!D(b))throw F("$http")("badreq",b);if(!C(p.valueOf(b.url)))throw F("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers=function(b){var c=a.headers,e=S({},b.headers),f,g,h,c=S({},c.common,c[K(b.method)]);a:for(f in c){g=K(f);for(h in e)if(K(h)===g)continue a;e[f]=c[f]}return d(e,ja(b))}(b);g.method=
vb(g.method);g.paramSerializer=C(g.paramSerializer)?m.get(g.paramSerializer):g.paramSerializer;e.$$incOutstandingRequestCount("$http");var h=[],k=[];b=l.resolve(g);r(v,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&k.push(a.response,a.responseError)});b=c(b,h);b=b.then(function(b){var c=b.headers,d=Bd(b.data,Ad(c),void 0,b.transformRequest);A(d)&&r(c,function(a,b){"content-type"===K(b)&&delete c[b]});A(b.withCredentials)&&!A(a.withCredentials)&&
(b.withCredentials=a.withCredentials);return s(b,d).then(f,f)});b=c(b,k);return b=b.finally(function(){e.$$completeOutstandingRequest(E,"$http")})}function s(c,d){function e(a){if(a){var c={};r(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function k(a,c,d,e,f){function g(){m(c,a,d,e,f)}R&&(200<=a&&300>a?R.put(O,[a,c,zd(d),e,f]):R.remove(O));b?h.$applyAsync(g):(g(),h.$$phase||h.$apply())}function m(a,b,d,e,f){b=-1<=b?b:0;(200<=b&&300>
b?L.resolve:L.reject)({data:a,status:b,headers:Ad(d),config:c,statusText:e,xhrStatus:f})}function s(a){m(a.data,a.status,ja(a.headers()),a.statusText,a.xhrStatus)}function v(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var L=l.defer(),u=L.promise,R,q,ma=c.headers,x="jsonp"===K(c.method),O=c.url;x?O=p.getTrustedResourceUrl(O):C(O)||(O=p.valueOf(O));O=G(O,c.paramSerializer(c.params));x&&(O=t(O,c.jsonpCallbackParam));n.pendingRequests.push(c);u.then(v,v);!c.cache&&!a.cache||
!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(R=D(c.cache)?c.cache:D(a.cache)?a.cache:N);R&&(q=R.get(O),w(q)?q&&B(q.then)?q.then(s,s):H(q)?m(q[1],q[0],ja(q[2]),q[3],q[4]):m(q,200,{},"OK","complete"):R.put(O,u));A(q)&&((q=kc(c.url)?g()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(ma[c.xsrfHeaderName||a.xsrfHeaderName]=q),f(c.method,O,d,k,ma,c.timeout,c.withCredentials,c.responseType,e(c.eventHandlers),e(c.uploadEventHandlers)));return u}function G(a,b){0<b.length&&(a+=(-1===a.indexOf("?")?
"?":"&")+b);return a}function t(a,b){var c=a.split("?");if(2<c.length)throw Lb("badjsonp",a);c=hc(c[1]);r(c,function(c,d){if("JSON_CALLBACK"===c)throw Lb("badjsonp",a);if(d===b)throw Lb("badjsonp",b,a);});return a+=(-1===a.indexOf("?")?"?":"&")+b+"=JSON_CALLBACK"}var N=k("$http");a.paramSerializer=C(a.paramSerializer)?m.get(a.paramSerializer):a.paramSerializer;var v=[];r(d,function(a){v.unshift(C(a)?m.get(a):m.invoke(a))});var kc=Og(c);n.pendingRequests=[];(function(a){r(arguments,function(a){n[a]=
function(b,c){return n(S({},c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){r(arguments,function(a){n[a]=function(b,c,d){return n(S({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");n.defaults=a;return n}]}function Wf(){this.$get=function(){return function(){return new z.XMLHttpRequest}}}function Vf(){this.$get=["$browser","$jsonpCallbacks","$document","$xhrFactory",function(a,b,d,c){return Pg(a,c,a.defer,b,d[0])}]}function Pg(a,b,d,c,e){function f(a,b,d){a=a.replace("JSON_CALLBACK",
b);var f=e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f.async=!0;m=function(a){f.removeEventListener("load",m);f.removeEventListener("error",m);e.body.removeChild(f);f=null;var g=-1,s="unknown";a&&("load"!==a.type||c.wasCalled(b)||(a={type:"error"}),s=a.type,g="error"===a.type?404:200);d&&d(g,s)};f.addEventListener("load",m);f.addEventListener("error",m);e.body.appendChild(f);return m}return function(e,k,h,l,m,p,n,s,G,t){function N(a){J="timeout"===a;pa&&pa();y&&y.abort()}function v(a,
b,c,e,f,g){w(P)&&d.cancel(P);pa=y=null;a(b,c,e,f,g)}k=k||a.url();if("jsonp"===K(e))var q=c.createCallback(k),pa=f(k,q,function(a,b){var d=200===a&&c.getResponse(q);v(l,a,d,"",b,"complete");c.removeCallback(q)});else{var y=b(e,k),J=!1;y.open(e,k,!0);r(m,function(a,b){w(a)&&y.setRequestHeader(b,a)});y.onload=function(){var a=y.statusText||"",b="response"in y?y.response:y.responseText,c=1223===y.status?204:y.status;0===c&&(c=b?200:"file"===ga(k).protocol?404:0);v(l,c,b,y.getAllResponseHeaders(),a,"complete")};
y.onerror=function(){v(l,-1,null,null,"","error")};y.ontimeout=function(){v(l,-1,null,null,"","timeout")};y.onabort=function(){v(l,-1,null,null,"",J?"timeout":"abort")};r(G,function(a,b){y.addEventListener(b,a)});r(t,function(a,b){y.upload.addEventListener(b,a)});n&&(y.withCredentials=!0);if(s)try{y.responseType=s}catch(I){if("json"!==s)throw I;}y.send(A(h)?null:h)}if(0<p)var P=d(function(){N("timeout")},p);else p&&B(p.then)&&p.then(function(){N(w(p.$$timeoutId)?"timeout":"abort")})}}function Pf(){var a=
"{{",b="}}";this.startSymbol=function(b){return b?(a=b,this):a};this.endSymbol=function(a){return a?(b=a,this):b};this.$get=["$parse","$exceptionHandler","$sce",function(d,c,e){function f(a){return"\\\\\\"+a}function g(c){return c.replace(p,a).replace(n,b)}function k(a,b,c,d){var e=a.$watch(function(a){e();return d(a)},b,c);return e}function h(f,h,n,p){function v(a){try{return a=n&&!r?e.getTrusted(n,a):e.valueOf(a),p&&!w(a)?a:jc(a)}catch(b){c(Ma.interr(f,b))}}var r=n===e.URL||n===e.MEDIA_URL;if(!f.length||
-1===f.indexOf(a)){if(h)return;h=g(f);r&&(h=e.getTrusted(n,h));h=ia(h);h.exp=f;h.expressions=[];h.$$watchDelegate=k;return h}p=!!p;for(var q,y,J=0,I=[],P,Q=f.length,M=[],L=[],u;J<Q;)if(-1!==(q=f.indexOf(a,J))&&-1!==(y=f.indexOf(b,q+l)))J!==q&&M.push(g(f.substring(J,q))),J=f.substring(q+l,y),I.push(J),J=y+m,L.push(M.length),M.push("");else{J!==Q&&M.push(g(f.substring(J)));break}u=1===M.length&&1===L.length;var R=r&&u?void 0:v;P=I.map(function(a){return d(a,R)});if(!h||I.length){var x=function(a){for(var b=
0,c=I.length;b<c;b++){if(p&&A(a[b]))return;M[L[b]]=a[b]}if(r)return e.getTrusted(n,u?M[0]:M.join(""));n&&1<M.length&&Ma.throwNoconcat(f);return M.join("")};return S(function(a){var b=0,d=I.length,e=Array(d);try{for(;b<d;b++)e[b]=P[b](a);return x(e)}catch(g){c(Ma.interr(f,g))}},{exp:f,expressions:I,$$watchDelegate:function(a,b){var c;return a.$watchGroup(P,function(d,e){var f=x(d);b.call(this,f,d!==e?c:f,a);c=f})}})}}var l=a.length,m=b.length,p=new RegExp(a.replace(/./g,f),"g"),n=new RegExp(b.replace(/./g,
f),"g");h.startSymbol=function(){return a};h.endSymbol=function(){return b};return h}]}function Qf(){this.$get=["$$intervalFactory","$window",function(a,b){var d={},c=function(a){b.clearInterval(a);delete d[a]},e=a(function(a,c,e){a=b.setInterval(a,c);d[a]=e;return a},c);e.cancel=function(a){if(!a)return!1;if(!a.hasOwnProperty("$$intervalId"))throw Qg("badprom");if(!d.hasOwnProperty(a.$$intervalId))return!1;a=a.$$intervalId;var b=d[a],e=b.promise;e.$$state&&(e.$$state.pur=!0);b.reject("canceled");
c(a);return!0};return e}]}function Rf(){this.$get=["$browser","$q","$$q","$rootScope",function(a,b,d,c){return function(e,f){return function(g,k,h,l){function m(){p?g.apply(null,n):g(s)}var p=4<arguments.length,n=p?Ha.call(arguments,4):[],s=0,G=w(l)&&!l,t=(G?d:b).defer(),r=t.promise;h=w(h)?h:0;r.$$intervalId=e(function(){G?a.defer(m):c.$evalAsync(m);t.notify(s++);0<h&&s>=h&&(t.resolve(s),f(r.$$intervalId));G||c.$apply()},k,t,G);return r}}}]}function Cd(a,b){var d=ga(a);b.$$protocol=d.protocol;b.$$host=
d.hostname;b.$$port=fa(d.port)||Rg[d.protocol]||null}function Dd(a,b,d){if(Sg.test(a))throw kb("badpath",a);var c="/"!==a.charAt(0);c&&(a="/"+a);a=ga(a);for(var c=(c&&"/"===a.pathname.charAt(0)?a.pathname.substring(1):a.pathname).split("/"),e=c.length;e--;)c[e]=decodeURIComponent(c[e]),d&&(c[e]=c[e].replace(/\//g,"%2F"));d=c.join("/");b.$$path=d;b.$$search=hc(a.search);b.$$hash=decodeURIComponent(a.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function yc(a,b){return a.slice(0,
b.length)===b}function ya(a,b){if(yc(b,a))return b.substr(a.length)}function Da(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function zc(a,b,d){this.$$html5=!0;d=d||"";Cd(a,this);this.$$parse=function(a){var d=ya(b,a);if(!C(d))throw kb("ipthprfx",a,b);Dd(d,this,!0);this.$$path||(this.$$path="/");this.$$compose()};this.$$normalizeUrl=function(a){return b+a.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;w(f=ya(a,c))?(g=f,g=d&&w(f=ya(d,f))?
b+(ya("/",f)||f):a+g):w(f=ya(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function Ac(a,b,d){Cd(a,this);this.$$parse=function(c){var e=ya(a,c)||ya(b,c),f;A(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",A(e)&&(a=c,this.replace())):(f=ya(d,e),A(f)&&(f=e));Dd(f,this,!1);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;yc(f,e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$normalizeUrl=function(b){return a+(b?d+b:"")};this.$$parseLinkUrl=function(b,
d){return Da(a)===Da(b)?(this.$$parse(b),!0):!1}}function Ed(a,b,d){this.$$html5=!0;Ac.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a===Da(c)?f=c:(g=ya(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$normalizeUrl=function(b){return a+d+b}}function Mb(a){return function(){return this[a]}}function Fd(a,b){return function(d){if(A(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Yf(){var a="!",
b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return w(b)?(a=b,this):a};this.html5Mode=function(a){if(Ga(a))return b.enabled=a,this;if(D(a)){Ga(a.enabled)&&(b.enabled=a.enabled);Ga(a.requireBase)&&(b.requireBase=a.requireBase);if(Ga(a.rewriteLinks)||C(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function k(a,b){return a===b||ga(a).href===ga(b).href}function h(a,
b,d){var e=m.url(),f=m.$$state;try{c.url(a,b,d),m.$$state=c.state()}catch(g){throw m.url(e),m.$$state=f,g;}}function l(a,b){d.$broadcast("$locationChangeSuccess",m.absUrl(),a,m.$$state,b)}var m,p;p=c.baseHref();var n=c.url(),s;if(b.enabled){if(!p&&b.requireBase)throw kb("nobase");s=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(p||"/");p=e.history?zc:Ed}else s=Da(n),p=Ac;var r=s.substr(0,Da(s).lastIndexOf("/")+1);m=new p(s,r,"#"+a);m.$$parseLinkUrl(n,n);m.$$state=c.state();var t=/^\s*(javascript|mailto):/i;
f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!==a.which&&2!==a.button){for(var g=x(a.target);"a"!==ua(g[0]);)if(g[0]===f[0]||!(g=g.parent())[0])return;if(!C(e)||!A(g.attr(e))){var e=g.prop("href"),h=g.attr("href")||g.attr("xlink:href");D(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=ga(e.animVal).href);t.test(e)||!e||g.attr("target")||a.isDefaultPrevented()||!m.$$parseLinkUrl(e,h)||(a.preventDefault(),m.absUrl()!==c.url()&&d.$apply())}}});m.absUrl()!==
n&&c.url(m.absUrl(),!0);var N=!0;c.onUrlChange(function(a,b){yc(a,r)?(d.$evalAsync(function(){var c=m.absUrl(),e=m.$$state,f;m.$$parse(a);m.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;m.absUrl()===a&&(f?(m.$$parse(c),m.$$state=e,h(c,!1,e)):(N=!1,l(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(N||m.$$urlUpdatedByLocation){m.$$urlUpdatedByLocation=!1;var a=c.url(),b=m.absUrl(),f=c.state(),g=m.$$replace,n=!k(a,b)||m.$$html5&&e.history&&f!==
m.$$state;if(N||n)N=!1,d.$evalAsync(function(){var b=m.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,m.$$state,f).defaultPrevented;m.absUrl()===b&&(c?(m.$$parse(a),m.$$state=f):(n&&h(b,g,f===m.$$state?null:m.$$state),l(a,f)))})}m.$$replace=!1});return m}]}function Zf(){var a=!0,b=this;this.debugEnabled=function(b){return w(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){dc(a)&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&
(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||E;return function(){var a=[];r(arguments,function(b){a.push(c(b))});return Function.prototype.apply.call(e,b,a)}}var f=wa||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Tg(a){return a+""}function Ug(a,b){return"undefined"!==typeof a?a:
b}function Gd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function Vg(a,b){switch(a.type){case q.MemberExpression:if(a.computed)return!1;break;case q.UnaryExpression:return 1;case q.BinaryExpression:return"+"!==a.operator?1:!1;case q.CallExpression:return!1}return void 0===b?Hd:b}function Z(a,b,d){var c,e,f=a.isPure=Vg(a,d);switch(a.type){case q.Program:c=!0;r(a.body,function(a){Z(a.expression,b,f);c=c&&a.expression.constant});a.constant=c;break;case q.Literal:a.constant=!0;a.toWatch=
[];break;case q.UnaryExpression:Z(a.argument,b,f);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case q.BinaryExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case q.LogicalExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case q.ConditionalExpression:Z(a.test,b,f);Z(a.alternate,b,f);Z(a.consequent,b,f);a.constant=a.test.constant&&
a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case q.Identifier:a.constant=!1;a.toWatch=[a];break;case q.MemberExpression:Z(a.object,b,f);a.computed&&Z(a.property,b,f);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=a.constant?[]:[a];break;case q.CallExpression:c=d=a.filter?!b(a.callee.name).$stateful:!1;e=[];r(a.arguments,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=d?e:[a];break;case q.AssignmentExpression:Z(a.left,
b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case q.ArrayExpression:c=!0;e=[];r(a.elements,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=e;break;case q.ObjectExpression:c=!0;e=[];r(a.properties,function(a){Z(a.value,b,f);c=c&&a.value.constant;e.push.apply(e,a.value.toWatch);a.computed&&(Z(a.key,b,!1),c=c&&a.key.constant,e.push.apply(e,a.key.toWatch))});a.constant=c;a.toWatch=e;break;case q.ThisExpression:a.constant=
!1;a.toWatch=[];break;case q.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Id(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Jd(a){return a.type===q.Identifier||a.type===q.MemberExpression}function Kd(a){if(1===a.body.length&&Jd(a.body[0].expression))return{type:q.AssignmentExpression,left:a.body[0].expression,right:{type:q.NGValueParameter},operator:"="}}function Ld(a){this.$filter=a}function Md(a){this.$filter=a}function Nb(a,b,d){this.ast=
new q(a,d);this.astCompiler=d.csp?new Md(b):new Ld(b)}function Bc(a){return B(a.valueOf)?a.valueOf():Wg.call(a)}function $f(){var a=T(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(b,c){var d,f;switch(typeof b){case "string":return f=b=b.trim(),d=a[f],d||(d=new Ob(G),d=(new Nb(d,e,G)).parse(b),a[f]=p(d)),s(d,c);case "function":return s(b,c);default:return s(E,
c)}}function g(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=Bc(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function k(a,b,c,d,e){var f=d.inputs,h;if(1===f.length){var k=g,f=f[0];return a.$watch(function(a){var b=f(a);g(b,k,f.isPure)||(h=d(a,void 0,void 0,[b]),k=b&&Bc(b));return h},b,c,e)}for(var l=[],m=[],n=0,p=f.length;n<p;n++)l[n]=g,m[n]=null;return a.$watch(function(a){for(var b=!1,c=0,e=f.length;c<e;c++){var k=f[c](a);if(b||(b=!g(k,l[c],f[c].isPure)))m[c]=k,l[c]=k&&Bc(k)}b&&
(h=d(a,void 0,void 0,m));return h},b,c,e)}function h(a,b,c,d,e){function f(){h(m)&&k()}function g(a,b,c,d){m=u&&d?d[0]:n(a,b,c,d);h(m)&&a.$$postDigest(f);return s(m)}var h=d.literal?l:w,k,m,n=d.$$intercepted||d,s=d.$$interceptor||Ta,u=d.inputs&&!n.inputs;g.literal=d.literal;g.constant=d.constant;g.inputs=d.inputs;p(g);return k=a.$watch(g,b,c,e)}function l(a){var b=!0;r(a,function(a){w(a)||(b=!1)});return b}function m(a,b,c,d){var e=a.$watch(function(a){e();return d(a)},b,c);return e}function p(a){a.constant?
a.$$watchDelegate=m:a.oneTime?a.$$watchDelegate=h:a.inputs&&(a.$$watchDelegate=k);return a}function n(a,b){function c(d){return b(a(d))}c.$stateful=a.$stateful||b.$stateful;c.$$pure=a.$$pure&&b.$$pure;return c}function s(a,b){if(!b)return a;a.$$interceptor&&(b=n(a.$$interceptor,b),a=a.$$intercepted);var c=!1,d=function(d,e,f,g){d=c&&g?g[0]:a(d,e,f,g);return b(d)};d.$$intercepted=a;d.$$interceptor=b;d.literal=a.literal;d.oneTime=a.oneTime;d.constant=a.constant;b.$stateful||(c=!a.inputs,d.inputs=a.inputs?
a.inputs:[a],b.$$pure||(d.inputs=d.inputs.map(function(a){return a.isPure===Hd?function(b){return a(b)}:a})));return p(d)}var G={csp:Ba().noUnsafeEval,literals:Ia(b),isIdentifierStart:B(d)&&d,isIdentifierContinue:B(c)&&c};f.$$getAst=function(a){var b=new Ob(G);return(new Nb(b,e,G)).getAst(a).ast};return f}]}function bg(){var a=!0;this.$get=["$rootScope","$exceptionHandler",function(b,d){return Nd(function(a){b.$evalAsync(a)},d,a)}];this.errorOnUnhandledRejections=function(b){return w(b)?(a=b,this):
a}}function cg(){var a=!0;this.$get=["$browser","$exceptionHandler",function(b,d){return Nd(function(a){b.defer(a)},d,a)}];this.errorOnUnhandledRejections=function(b){return w(b)?(a=b,this):a}}function Nd(a,b,d){function c(){return new e}function e(){var a=this.promise=new f;this.resolve=function(b){h(a,b)};this.reject=function(b){m(a,b)};this.notify=function(b){n(a,b)}}function f(){this.$$state={status:0}}function g(){for(;!w&&x.length;){var a=x.shift();if(!a.pur){a.pur=!0;var c=a.value,c="Possibly unhandled rejection: "+
("function"===typeof c?c.toString().replace(/ \{[\s\S]*$/,""):A(c)?"undefined":"string"!==typeof c?Ne(c,void 0):c);dc(a.value)?b(a.value,c):b(c)}}}function k(c){!d||c.pending||2!==c.status||c.pur||(0===w&&0===x.length&&a(g),x.push(c));!c.processScheduled&&c.pending&&(c.processScheduled=!0,++w,a(function(){var e,f,k;k=c.pending;c.processScheduled=!1;c.pending=void 0;try{for(var l=0,n=k.length;l<n;++l){c.pur=!0;f=k[l][0];e=k[l][c.status];try{B(e)?h(f,e(c.value)):1===c.status?h(f,c.value):m(f,c.value)}catch(p){m(f,
p),p&&!0===p.$$passToExceptionHandler&&b(p)}}}finally{--w,d&&0===w&&a(g)}}))}function h(a,b){a.$$state.status||(b===a?p(a,v("qcycle",b)):l(a,b))}function l(a,b){function c(b){g||(g=!0,l(a,b))}function d(b){g||(g=!0,p(a,b))}function e(b){n(a,b)}var f,g=!1;try{if(D(b)||B(b))f=b.then;B(f)?(a.$$state.status=-1,f.call(b,c,d,e)):(a.$$state.value=b,a.$$state.status=1,k(a.$$state))}catch(h){d(h)}}function m(a,b){a.$$state.status||p(a,b)}function p(a,b){a.$$state.value=b;a.$$state.status=2;k(a.$$state)}function n(c,
d){var e=c.$$state.pending;0>=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;f<g;f++){c=e[f][0];a=e[f][3];try{n(c,B(a)?a(d):d)}catch(h){b(h)}}})}function s(a){var b=new f;m(b,a);return b}function G(a,b,c){var d=null;try{B(c)&&(d=c())}catch(e){return s(e)}return d&&B(d.then)?d.then(function(){return b(a)},s):b(a)}function t(a,b,c,d){var e=new f;h(e,a);return e.then(b,c,d)}function q(a){if(!B(a))throw v("norslvr",a);var b=new f;a(function(a){h(b,a)},function(a){m(b,a)});return b}
var v=F("$q",TypeError),w=0,x=[];S(f.prototype,{then:function(a,b,c){if(A(a)&&A(b)&&A(c))return this;var d=new f;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&k(this.$$state);return d},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return G(b,y,a)},function(b){return G(b,s,a)},b)}});var y=t;q.prototype=f.prototype;q.defer=c;q.reject=s;q.when=t;q.resolve=y;q.all=function(a){var b=new f,c=
0,d=H(a)?[]:{};r(a,function(a,e){c++;t(a).then(function(a){d[e]=a;--c||h(b,d)},function(a){m(b,a)})});0===c&&h(b,d);return b};q.race=function(a){var b=c();r(a,function(a){t(a).then(b.resolve,b.reject)});return b.promise};return q}function mg(){this.$get=["$window","$timeout",function(a,b){var d=a.requestAnimationFrame||a.webkitRequestAnimationFrame,c=a.cancelAnimationFrame||a.webkitCancelAnimationFrame||a.webkitCancelRequestAnimationFrame,e=!!d,f=e?function(a){var b=d(a);return function(){c(b)}}:
function(a){var c=b(a,16.66,!1);return function(){b.cancel(c)}};f.supported=e;return f}]}function ag(){function a(a){function b(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$id=++qb;this.$$ChildScope=null;this.$$suspended=!1}b.prototype=a;return b}var b=10,d=F("$rootScope"),c=null,e=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$exceptionHandler","$parse","$browser",
function(f,g,k){function h(a){a.currentScope.$$destroyed=!0}function l(a){9===wa&&(a.$$childHead&&l(a.$$childHead),a.$$nextSibling&&l(a.$$nextSibling));a.$parent=a.$$nextSibling=a.$$prevSibling=a.$$childHead=a.$$childTail=a.$root=a.$$watchers=null}function m(){this.$id=++qb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=this;this.$$suspended=this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=
0;this.$$isolateBindings=null}function p(a){if(v.$$phase)throw d("inprog",v.$$phase);v.$$phase=a}function n(a,b){do a.$$watchersCount+=b;while(a=a.$parent)}function s(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function G(){}function t(){for(;y.length;)try{y.shift()()}catch(a){f(a)}e=null}function q(){null===e&&(e=k.defer(function(){v.$apply(t)},null,"$applyAsync"))}m.prototype={constructor:m,$new:function(b,c){var d;c=c||this;b?(d=new m,
d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=a(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(b||c!==this)&&d.$on("$destroy",h);return d},$watch:function(a,b,d,e){var f=g(a);b=B(b)?b:E;if(f.$$watchDelegate)return f.$$watchDelegate(this,b,d,f,a);var h=this,k=h.$$watchers,l={fn:b,last:G,get:f,exp:e||a,eq:!!d};c=null;k||(k=h.$$watchers=[],k.$$digestWatchIndex=-1);k.unshift(l);
k.$$digestWatchIndex++;n(this,1);return function(){var a=cb(k,l);0<=a&&(n(h,-1),a<k.$$digestWatchIndex&&k.$$digestWatchIndex--);c=null}},$watchGroup:function(a,b){function c(){h=!1;try{k?(k=!1,b(e,e,g)):b(e,d,g)}finally{for(var f=0;f<a.length;f++)d[f]=e[f]}}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,k=!0;if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});r(a,function(a,
b){var d=g.$watch(a,function(a){e[b]=a;h||(h=!0,g.$evalAsync(c))});f.push(d)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!A(e)){if(D(e))if(za(e))for(f!==n&&(f=n,t=f.length=0,l++),a=e.length,t!==a&&(l++,f.length=t=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==p&&(f=p={},t=0,l++);a=0;for(b in e)ta.call(e,b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(t++,f[b]=g,l++));if(t>a)for(b in l++,
f)ta.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$$pure=g(a).literal;c.$stateful=!c.$$pure;var d=this,e,f,h,k=1<b.length,l=0,m=g(a,c),n=[],p={},s=!0,t=0;return this.$watch(m,function(){s?(s=!1,b(e,e,d)):b(e,h,d);if(k)if(D(e))if(za(e)){h=Array(e.length);for(var a=0;a<e.length;a++)h[a]=e[a]}else for(a in h={},e)ta.call(e,a)&&(h[a]=e[a]);else h=e})},$digest:function(){var a,g,h,l,m,n,s,r=b,q,y=w.length?v:this,N=[],A,z;p("$digest");k.$$checkUrlChange();this===v&&null!==e&&(k.defer.cancel(e),
t());c=null;do{s=!1;q=y;for(n=0;n<w.length;n++){try{z=w[n],l=z.fn,l(z.scope,z.locals)}catch(C){f(C)}c=null}w.length=0;a:do{if(n=!q.$$suspended&&q.$$watchers)for(n.$$digestWatchIndex=n.length;n.$$digestWatchIndex--;)try{if(a=n[n.$$digestWatchIndex])if(m=a.get,(g=m(q))!==(h=a.last)&&!(a.eq?va(g,h):Y(g)&&Y(h)))s=!0,c=a,a.last=a.eq?Ia(g,null):g,l=a.fn,l(g,h===G?g:h,q),5>r&&(A=4-r,N[A]||(N[A]=[]),N[A].push({msg:B(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:h}));else if(a===c){s=
!1;break a}}catch(E){f(E)}if(!(n=!q.$$suspended&&q.$$watchersCount&&q.$$childHead||q!==y&&q.$$nextSibling))for(;q!==y&&!(n=q.$$nextSibling);)q=q.$parent}while(q=n);if((s||w.length)&&!r--)throw v.$$phase=null,d("infdig",b,N);}while(s||w.length);for(v.$$phase=null;J<x.length;)try{x[J++]()}catch(D){f(D)}x.length=J=0;k.$$checkUrlChange()},$suspend:function(){this.$$suspended=!0},$isSuspended:function(){return this.$$suspended},$resume:function(){this.$$suspended=!1},$destroy:function(){if(!this.$$destroyed){var a=
this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===v&&k.$$applicationDestroyed();n(this,-this.$$watchersCount);for(var b in this.$$listenerCount)s(this,this.$$listenerCount[b],b);a&&a.$$childHead===this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail===this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=
this.$evalAsync=this.$applyAsync=E;this.$on=this.$watch=this.$watchGroup=function(){return E};this.$$listeners={};this.$$nextSibling=null;l(this)}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a,b){v.$$phase||w.length||k.defer(function(){w.length&&v.$digest()},null,"$evalAsync");w.push({scope:this,fn:g(a),locals:b})},$$postDigest:function(a){x.push(a)},$apply:function(a){try{p("$apply");try{return this.$eval(a)}finally{v.$$phase=null}}catch(b){f(b)}finally{try{v.$digest()}catch(c){throw f(c),
c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&y.push(b);a=g(a);q()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(delete c[d],s(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,g=!1,h={name:a,targetScope:e,stopPropagation:function(){g=!0},preventDefault:function(){h.defaultPrevented=
!0},defaultPrevented:!1},k=db([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;l<m;l++)if(d[l])try{d[l].apply(null,k)}catch(n){f(n)}else d.splice(l,1),l--,m--;if(g)break;e=e.$parent}while(e);h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var g=db([e],arguments,1),h,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||
[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,g)}catch(l){f(l)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var v=new m,w=v.$$asyncQueue=[],x=v.$$postDigestQueue=[],y=v.$$applyAsyncQueue=[],J=0;return v}]}function Qe(){var a=/^\s*(https?|s?ftp|mailto|tel|file):/,b=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationTrustedUrlList=function(b){return w(b)?
(a=b,this):a};this.imgSrcSanitizationTrustedUrlList=function(a){return w(a)?(b=a,this):b};this.$get=function(){return function(d,c){var e=c?b:a,f=ga(d&&d.trim()).href;return""===f||f.match(e)?d:"unsafe:"+f}}}function Xg(a){if("self"===a)return a;if(C(a)){if(-1<a.indexOf("***"))throw Ea("iwcard",a);a=Od(a).replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^:/.?&;]*");return new RegExp("^"+a+"$")}if(ab(a))return new RegExp("^"+a.source+"$");throw Ea("imatcher");}function Pd(a){var b=[];w(a)&&r(a,function(a){b.push(Xg(a))});
return b}function eg(){this.SCE_CONTEXTS=W;var a=["self"],b=[];this.trustedResourceUrlList=function(b){arguments.length&&(a=Pd(b));return a};Object.defineProperty(this,"resourceUrlWhitelist",{get:function(){return this.trustedResourceUrlList},set:function(a){this.trustedResourceUrlList=a}});this.bannedResourceUrlList=function(a){arguments.length&&(b=Pd(a));return b};Object.defineProperty(this,"resourceUrlBlacklist",{get:function(){return this.bannedResourceUrlList},set:function(a){this.bannedResourceUrlList=
a}});this.$get=["$injector","$$sanitizeUri",function(d,c){function e(a,b){var c;"self"===a?(c=Cc(b,Qd))||(z.document.baseURI?c=z.document.baseURI:(Na||(Na=z.document.createElement("a"),Na.href=".",Na=Na.cloneNode(!1)),c=Na.href),c=Cc(b,c)):c=!!a.exec(b.href);return c}function f(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};
return b}var g=function(a){throw Ea("unsafe");};d.has("$sanitize")&&(g=d.get("$sanitize"));var k=f(),h={};h[W.HTML]=f(k);h[W.CSS]=f(k);h[W.MEDIA_URL]=f(k);h[W.URL]=f(h[W.MEDIA_URL]);h[W.JS]=f(k);h[W.RESOURCE_URL]=f(h[W.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw Ea("icontext",a,b);if(null===b||A(b)||""===b)return b;if("string"!==typeof b)throw Ea("itype",a);return new c(b)},getTrusted:function(d,f){if(null===f||A(f)||""===f)return f;var k=h.hasOwnProperty(d)?
h[d]:null;if(k&&f instanceof k)return f.$$unwrapTrustedValue();B(f.$$unwrapTrustedValue)&&(f=f.$$unwrapTrustedValue());if(d===W.MEDIA_URL||d===W.URL)return c(f.toString(),d===W.MEDIA_URL);if(d===W.RESOURCE_URL){var k=ga(f.toString()),n,s,r=!1;n=0;for(s=a.length;n<s;n++)if(e(a[n],k)){r=!0;break}if(r)for(n=0,s=b.length;n<s;n++)if(e(b[n],k)){r=!1;break}if(r)return f;throw Ea("insecurl",f.toString());}if(d===W.HTML)return g(f);throw Ea("unsafe");},valueOf:function(a){return a instanceof k?a.$$unwrapTrustedValue():
a}}}]}function dg(){var a=!0;this.enabled=function(b){arguments.length&&(a=!!b);return a};this.$get=["$parse","$sceDelegate",function(b,d){if(a&&8>wa)throw Ea("iequirks");var c=ja(W);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ta);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;r(W,
function(a,b){var d=K(b);c[("parse_as_"+d).replace(Dc,xb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(Dc,xb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(Dc,xb)]=function(b){return g(a,b)}});return c}]}function fg(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=fa((/android (\d+)/.exec(K((a.navigator||{}).userAgent))||
[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},k=g.body&&g.body.style,h=!1,l=!1;k&&(h=!!("transition"in k||"webkitTransition"in k),l=!!("animation"in k||"webkitAnimation"in k));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&wa)return!1;if(A(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ba(),transitions:h,animations:l,android:e}}]}function gg(){this.$get=ia(function(a){return new Yg(a)})}function Yg(a){function b(){var a=e.pop();return a&&
a.cb}function d(a){for(var b=e.length-1;0<=b;--b){var c=e[b];if(c.type===a)return e.splice(b,1),c.cb}}var c={},e=[],f=this.ALL_TASKS_TYPE="$$all$$",g=this.DEFAULT_TASK_TYPE="$$default$$";this.completeTask=function(e,h){h=h||g;try{e()}finally{var l;l=h||g;c[l]&&(c[l]--,c[f]--);l=c[h];var m=c[f];if(!m||!l)for(l=m?d:b;m=l(h);)try{m()}catch(p){a.error(p)}}};this.incTaskCount=function(a){a=a||g;c[a]=(c[a]||0)+1;c[f]=(c[f]||0)+1};this.notifyWhenNoPendingTasks=function(a,b){b=b||f;c[b]?e.push({type:b,cb:a}):
a()}}function ig(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler","$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(k,h){g.totalPendingRequests++;if(!C(k)||A(d.get(k)))k=f.getTrustedResourceUrl(k);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==wc}):l===wc&&(l=null);return c.get(k,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){return d.put(k,a.data)},
function(a){h||(a=Zg("tpload",k,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=0;return g}]}function jg(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var c=ca.element(a).data("$binding");c&&r(c,function(c){d?(new RegExp("(^|\\s)"+Od(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-",
"data-ng-","ng\\:"],k=0;k<g.length;++k){var h=a.querySelectorAll("["+g[k]+"model"+(d?"=":"*=")+'"'+b+'"]');if(h.length)return h}},getLocation:function(){return d.url()},setLocation:function(b){b!==d.url()&&(d.url(b),a.$digest())},whenStable:function(a){b.notifyWhenNoOutstandingRequests(a)}}}]}function kg(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(a,b,d,c,e){function f(f,h,l){B(f)||(l=h,h=f,f=E);var m=Ha.call(arguments,3),p=w(l)&&!l,n=(p?c:d).defer(),s=n.promise,r;
r=b.defer(function(){try{n.resolve(f.apply(null,m))}catch(b){n.reject(b),e(b)}finally{delete g[s.$$timeoutId]}p||a.$apply()},h,"$timeout");s.$$timeoutId=r;g[r]=n;return s}var g={};f.cancel=function(a){if(!a)return!1;if(!a.hasOwnProperty("$$timeoutId"))throw $g("badprom");if(!g.hasOwnProperty(a.$$timeoutId))return!1;a=a.$$timeoutId;var c=g[a],d=c.promise;d.$$state&&(d.$$state.pur=!0);c.reject("canceled");delete g[a];return b.defer.cancel(a)};return f}]}function ga(a){if(!C(a))return a;wa&&(aa.setAttribute("href",
a),a=aa.href);aa.setAttribute("href",a);a=aa.hostname;!ah&&-1<a.indexOf(":")&&(a="["+a+"]");return{href:aa.href,protocol:aa.protocol?aa.protocol.replace(/:$/,""):"",host:aa.host,search:aa.search?aa.search.replace(/^\?/,""):"",hash:aa.hash?aa.hash.replace(/^#/,""):"",hostname:a,port:aa.port,pathname:"/"===aa.pathname.charAt(0)?aa.pathname:"/"+aa.pathname}}function Og(a){var b=[Qd].concat(a.map(ga));return function(a){a=ga(a);return b.some(Cc.bind(null,a))}}function Cc(a,b){a=ga(a);b=ga(b);return a.protocol===
b.protocol&&a.host===b.host}function lg(){this.$get=ia(z)}function Rd(a){function b(a){try{return decodeURIComponent(a)}catch(b){return a}}var d=a[0]||{},c={},e="";return function(){var a,g,k,h,l;try{a=d.cookie||""}catch(m){a=""}if(a!==e)for(e=a,a=e.split("; "),c={},k=0;k<a.length;k++)g=a[k],h=g.indexOf("="),0<h&&(l=b(g.substring(0,h)),A(c[l])&&(c[l]=b(g.substring(h+1))));return c}}function pg(){this.$get=Rd}function fd(a){function b(d,c){if(D(d)){var e={};r(d,function(a,c){e[c]=b(c,a)});return e}return a.factory(d+
"Filter",c)}this.register=b;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];b("currency",Sd);b("date",Td);b("filter",bh);b("json",ch);b("limitTo",dh);b("lowercase",eh);b("number",Ud);b("orderBy",Vd);b("uppercase",fh)}function bh(){return function(a,b,d,c){if(!za(a)){if(null==a)return a;throw F("filter")("notarray",a);}c=c||"$";var e;switch(Ec(b)){case "function":break;case "boolean":case "null":case "number":case "string":e=!0;case "object":b=gh(b,d,c,e);break;default:return a}return Array.prototype.filter.call(a,
b)}}function gh(a,b,d,c){var e=D(a)&&d in a;!0===b?b=va:B(b)||(b=function(a,b){if(A(a))return!1;if(null===a||null===b)return a===b;if(D(b)||D(a)&&!cc(a))return!1;a=K(""+a);b=K(""+b);return-1!==a.indexOf(b)});return function(f){return e&&!D(f)?Fa(f,a[d],b,d,!1):Fa(f,a,b,d,c)}}function Fa(a,b,d,c,e,f){var g=Ec(a),k=Ec(b);if("string"===k&&"!"===b.charAt(0))return!Fa(a,b.substring(1),d,c,e);if(H(a))return a.some(function(a){return Fa(a,b,d,c,e)});switch(g){case "object":var h;if(e){for(h in a)if(h.charAt&&
"$"!==h.charAt(0)&&Fa(a[h],b,d,c,!0))return!0;return f?!1:Fa(a,b,d,c,!1)}if("object"===k){for(h in b)if(f=b[h],!B(f)&&!A(f)&&(g=h===c,!Fa(g?a:a[h],f,d,c,g,g)))return!1;return!0}return d(a,b);case "function":return!1;default:return d(a,b)}}function Ec(a){return null===a?"null":typeof a}function Sd(a){var b=a.NUMBER_FORMATS;return function(a,c,e){A(c)&&(c=b.CURRENCY_SYM);A(e)&&(e=b.PATTERNS[1].maxFrac);var f=c?/\u00A4/g:/\s*\u00A4\s*/g;return null==a?a:Wd(a,b.PATTERNS[1],b.GROUP_SEP,b.DECIMAL_SEP,e).replace(f,
c)}}function Ud(a){var b=a.NUMBER_FORMATS;return function(a,c){return null==a?a:Wd(a,b.PATTERNS[0],b.GROUP_SEP,b.DECIMAL_SEP,c)}}function hh(a){var b=0,d,c,e,f,g;-1<(c=a.indexOf(Xd))&&(a=a.replace(Xd,""));0<(e=a.search(/e/i))?(0>c&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===Fc;e++);if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===Fc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Yd&&(d=d.splice(0,Yd-1),b=c-1,c=1);return{d:d,e:b,i:c}}function ih(a,
b,d,c){var e=a.d,f=e.length-a.i;b=A(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0<d){e.splice(Math.max(a.i,d));for(var g=d;g<e.length;g++)e[g]=0}else for(f=Math.max(0,f),a.i=1,e.length=Math.max(1,d=b+1),e[0]=0,g=1;g<d;g++)e[g]=0;if(5<=c)if(0>d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;f<Math.max(0,b);f++)e.push(0);if(b=e.reduceRight(function(a,b,c,d){b+=a;d[c]=b%10;return Math.floor(b/10)},0))e.unshift(b),a.i++}function Wd(a,b,d,c,e){if(!C(a)&&!X(a)||isNaN(a))return"";
var f=!isFinite(a),g=!1,k=Math.abs(a)+"",h="";if(f)h="\u221e";else{g=hh(k);ih(g,e,b.minFrac,b.maxFrac);h=g.d;k=g.i;e=g.e;f=[];for(g=h.reduce(function(a,b){return a&&!b},!0);0>k;)h.unshift(0),k++;0<k?f=h.splice(k,h.length):(f=h,h=[0]);k=[];for(h.length>=b.lgSize&&k.unshift(h.splice(-b.lgSize,h.length).join(""));h.length>b.gSize;)k.unshift(h.splice(-b.gSize,h.length).join(""));h.length&&k.unshift(h.join(""));h=k.join(d);f.length&&(h+=c+f.join(""));e&&(h+="e+"+e)}return 0>a&&!g?b.negPre+h+b.negSuf:b.posPre+
h+b.posSuf}function Pb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length<b;)a=Fc+a;d&&(a=a.substr(a.length-b));return e+a}function ea(a,b,d,c,e){d=d||0;return function(f){f=f["get"+a]();if(0<d||f>-d)f+=d;0===f&&-12===d&&(f=12);return Pb(f,b,c,e)}}function lb(a,b,d){return function(c,e){var f=c["get"+a](),g=vb((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Zd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function $d(a){return function(b){var d=
Zd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Pb(b,a)}}function Gc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Td(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,k=b[8]?a.setUTCFullYear:a.setFullYear,h=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=fa(b[9]+b[10]),g=fa(b[9]+b[11]));k.call(a,fa(b[1]),fa(b[2])-1,fa(b[3]));f=fa(b[4]||0)-f;g=fa(b[5]||0)-g;k=fa(b[6]||0);b=Math.round(1E3*parseFloat("0."+
(b[7]||0)));h.call(a,f,g,k,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",k=[],h,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;C(c)&&(c=jh.test(c)?fa(c):b(c));X(c)&&(c=new Date(c));if(!ha(c)||!isFinite(c.getTime()))return c;for(;d;)(l=kh.exec(d))?(k=db(k,l,1),d=k.pop()):(k.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=fc(f,m),c=gc(c,f,!0));r(k,function(b){h=lh[b];g+=h?h(c,a.DATETIME_FORMATS,
m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ch(){return function(a,b){A(b)&&(b=2);return eb(a,b)}}function dh(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):fa(b);if(Y(b))return a;X(a)&&(a=a.toString());if(!za(a))return a;d=!d||isNaN(d)?0:fa(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?Hc(a,d,d+b):0===d?Hc(a,b,a.length):Hc(a,Math.max(0,d+b),d)}}function Hc(a,b,d){return C(a)?a.slice(b,d):Ha.call(a,b,d)}function Vd(a){function b(b){return b.map(function(b){var c=
1,d=Ta;if(B(b))d=b;else if(C(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function c(a,b){var c=0,d=a.type,h=b.type;if(d===h){var h=a.value,l=b.value;"string"===d?(h=h.toLowerCase(),l=l.toLowerCase()):"object"===d&&(D(h)&&(h=a.index),D(l)&&(l=b.index));h!==l&&(c=
h<l?-1:1)}else c="undefined"===d?1:"undefined"===h?-1:"null"===d?1:"null"===h?-1:d<h?-1:1;return c}return function(a,f,g,k){if(null==a)return a;if(!za(a))throw F("orderBy")("notarray",a);H(f)||(f=[f]);0===f.length&&(f=["+"]);var h=b(f),l=g?-1:1,m=B(k)?k:c;a=Array.prototype.map.call(a,function(a,b){return{value:a,tieBreaker:{value:b,type:"number",index:b},predicateValues:h.map(function(c){var e=c.get(a);c=typeof e;if(null===e)c="null";else if("object"===c)a:{if(B(e.valueOf)&&(e=e.valueOf(),d(e)))break a;
cc(e)&&(e=e.toString(),d(e))}return{value:e,type:c,index:b}})}});a.sort(function(a,b){for(var d=0,e=h.length;d<e;d++){var f=m(a.predicateValues[d],b.predicateValues[d]);if(f)return f*h[d].descending*l}return(m(a.tieBreaker,b.tieBreaker)||c(a.tieBreaker,b.tieBreaker))*l});return a=a.map(function(a){return a.value})}}function Ra(a){B(a)&&(a={link:a});a.restrict=a.restrict||"AC";return ia(a)}function Qb(a,b,d,c,e){this.$$controls=[];this.$error={};this.$$success={};this.$pending=void 0;this.$name=e(b.name||
b.ngForm||"")(d);this.$dirty=!1;this.$valid=this.$pristine=!0;this.$submitted=this.$invalid=!1;this.$$parentForm=mb;this.$$element=a;this.$$animate=c;ae(this)}function ae(a){a.$$classCache={};a.$$classCache[be]=!(a.$$classCache[nb]=a.$$element.hasClass(nb))}function ce(a){function b(a,b,c){c&&!a.$$classCache[b]?(a.$$animate.addClass(a.$$element,b),a.$$classCache[b]=!0):!c&&a.$$classCache[b]&&(a.$$animate.removeClass(a.$$element,b),a.$$classCache[b]=!1)}function d(a,c,d){c=c?"-"+Xc(c,"-"):"";b(a,nb+
c,!0===d);b(a,be+c,!1===d)}var c=a.set,e=a.unset;a.clazz.prototype.$setValidity=function(a,g,k){A(g)?(this.$pending||(this.$pending={}),c(this.$pending,a,k)):(this.$pending&&e(this.$pending,a,k),de(this.$pending)&&(this.$pending=void 0));Ga(g)?g?(e(this.$error,a,k),c(this.$$success,a,k)):(c(this.$error,a,k),e(this.$$success,a,k)):(e(this.$error,a,k),e(this.$$success,a,k));this.$pending?(b(this,"ng-pending",!0),this.$valid=this.$invalid=void 0,d(this,"",null)):(b(this,"ng-pending",!1),this.$valid=
de(this.$error),this.$invalid=!this.$valid,d(this,"",this.$valid));g=this.$pending&&this.$pending[a]?void 0:this.$error[a]?!1:this.$$success[a]?!0:null;d(this,a,g);this.$$parentForm.$setValidity(a,g,this)}}function de(a){if(a)for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}function Ic(a){a.$formatters.push(function(b){return a.$isEmpty(b)?b:b.toString()})}function Sa(a,b,d,c,e,f){var g=K(b[0].type);if(!e.android){var k=!1;b.on("compositionstart",function(){k=!0});b.on("compositionupdate",
function(a){if(A(a.data)||""===a.data)k=!1});b.on("compositionend",function(){k=!1;l()})}var h,l=function(a){h&&(f.defer.cancel(h),h=null);if(!k){var e=b.val();a=a&&a.type;"password"===g||d.ngTrim&&"false"===d.ngTrim||(e=V(e));(c.$viewValue!==e||""===e&&c.$$hasNativeValidators)&&c.$setViewValue(e,a)}};if(e.hasEvent("input"))b.on("input",l);else{var m=function(a,b,c){h||(h=f.defer(function(){h=null;b&&b.value===c||l(a)}))};b.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||
m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut drop",m)}b.on("change",l);if(ee[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!h){var b=this.validity,c=b.badInput,d=b.typeMismatch;h=f.defer(function(){h=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Rb(a,b){return function(d,c){var e,f;if(ha(d))return d;if(C(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length-
1)&&(d=d.substring(1,d.length-1));if(mh.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},r(e,function(a,c){c<b.length&&(f[b[c]]=+a)}),e=new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0),100>f.yyyy&&e.setFullYear(f.yyyy),e}return NaN}}function ob(a,b,d,c){return function(e,f,g,k,h,l,m,
p){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function s(a){return w(a)&&!ha(a)?r(a)||void 0:a}function r(a,b){var c=k.$options.getOption("timezone");v&&v!==c&&(b=Uc(b,fc(v)));var e=d(a,b);!isNaN(e)&&c&&(e=gc(e,c));return e}Jc(e,f,g,k,a);Sa(e,f,g,k,h,l);var t="time"===a||"datetimelocal"===a,q,v;k.$parsers.push(function(c){if(k.$isEmpty(c))return null;if(b.test(c))return r(c,q);k.$$parserName=a});k.$formatters.push(function(a){if(a&&!ha(a))throw pb("datefmt",a);if(n(a)){q=a;var b=
k.$options.getOption("timezone");b&&(v=b,q=gc(q,b,!0));var d=c;t&&C(k.$options.getOption("timeSecondsFormat"))&&(d=c.replace("ss.sss",k.$options.getOption("timeSecondsFormat")).replace(/:$/,""));a=m("date")(a,d,b);t&&k.$options.getOption("timeStripZeroSeconds")&&(a=a.replace(/(?::00)?(?:\.000)?$/,""));return a}v=q=null;return""});if(w(g.min)||g.ngMin){var x=g.min||p(g.ngMin)(e),z=s(x);k.$validators.min=function(a){return!n(a)||A(z)||d(a)>=z};g.$observe("min",function(a){a!==x&&(z=s(a),x=a,k.$validate())})}if(w(g.max)||
g.ngMax){var y=g.max||p(g.ngMax)(e),J=s(y);k.$validators.max=function(a){return!n(a)||A(J)||d(a)<=J};g.$observe("max",function(a){a!==y&&(J=s(a),y=a,k.$validate())})}}}function Jc(a,b,d,c,e){(c.$$hasNativeValidators=D(b[0].validity))&&c.$parsers.push(function(a){var d=b.prop("validity")||{};if(d.badInput||d.typeMismatch)c.$$parserName=e;else return a})}function fe(a){a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(nh.test(b))return parseFloat(b);a.$$parserName="number"});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!X(b))throw pb("numfmt",
b);b=b.toString()}return b})}function na(a){w(a)&&!X(a)&&(a=parseFloat(a));return Y(a)?void 0:a}function Kc(a){var b=a.toString(),d=b.indexOf(".");return-1===d?-1<a&&1>a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ge(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Kc(a):0,k=e?Kc(b):0,h=f?Kc(d):0,g=Math.max(g,k,h),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function he(a,b,d,c,e){if(w(c)){a=
a(c);if(!a.constant)throw pb("constexpr",d,c);return a(b)}return e}function Lc(a,b){function d(a,b){if(!a||!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],m=0;m<b.length;m++)if(e===b[m])continue a;c.push(e)}return c}function c(a){if(!a)return a;var b=a;H(a)?b=a.map(c).join(" "):D(a)?b=Object.keys(a).filter(function(b){return a[b]}).join(" "):C(a)||(b=a+"");return b}a="ngClass"+a;var e;return["$parse",function(f){return{restrict:"AC",link:function(g,
k,h){function l(a,b){var c=[];r(a,function(a){if(0<b||p[a])p[a]=(p[a]||0)+b,p[a]===+(0<b)&&c.push(a)});return c.join(" ")}function m(a){if(a===b){var c=s,c=l(c&&c.split(" "),1);h.$addClass(c)}else c=s,c=l(c&&c.split(" "),-1),h.$removeClass(c);n=a}var p=k.data("$classCounts"),n=!0,s;p||(p=T(),k.data("$classCounts",p));"ngClass"!==a&&(e||(e=f("$index",function(a){return a&1})),g.$watch(e,m));g.$watch(f(h[a],c),function(a){if(n===b){var c=s&&s.split(" "),e=a&&a.split(" "),f=d(c,e),c=d(e,c),f=l(f,-1),
c=l(c,1);h.$addClass(c);h.$removeClass(f)}s=a})}}}]}function sd(a,b,d,c,e,f){return{restrict:"A",compile:function(g,k){var h=a(k[c]);return function(a,c){c.on(e,function(c){var e=function(){h(a,{$event:c})};if(b.$$phase)if(f)a.$evalAsync(e);else try{e()}catch(g){d(g)}else a.$apply(e)})}}}}function Sb(a,b,d,c,e,f,g,k,h){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=void 0;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=
[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=void 0;this.$name=h(d.name||"",!1)(a);this.$$parentForm=mb;this.$options=Tb;this.$$updateEvents="";this.$$updateEventHandler=this.$$updateEventHandler.bind(this);this.$$parsedNgModel=e(d.ngModel);this.$$parsedNgModelAssign=this.$$parsedNgModel.assign;this.$$ngModelGet=this.$$parsedNgModel;this.$$ngModelSet=this.$$parsedNgModelAssign;this.$$pendingDebounce=
null;this.$$parserValid=void 0;this.$$parserName="parse";this.$$currentValidationRunId=0;this.$$scope=a;this.$$rootScope=a.$root;this.$$attr=d;this.$$element=c;this.$$animate=f;this.$$timeout=g;this.$$parse=e;this.$$q=k;this.$$exceptionHandler=b;ae(this);oh(this)}function oh(a){a.$$scope.$watch(function(b){b=a.$$ngModelGet(b);b===a.$modelValue||a.$modelValue!==a.$modelValue&&b!==b||a.$$setModelValue(b);return b})}function Mc(a){this.$$options=a}function ie(a,b){r(b,function(b,c){w(a[c])||(a[c]=b)})}
function Oa(a,b){a.prop("selected",b);a.attr("selected",b)}function je(a,b,d){if(a){C(a)&&(a=new RegExp("^"+a+"$"));if(!a.test)throw F("ngPattern")("noregexp",b,a,Aa(d));return a}}function Ub(a){a=fa(a);return Y(a)?-1:a}var Xb={objectMaxDepth:5,urlErrorParamsEnabled:!0},ke=/^\/(.+)\/([a-z]*)$/,ta=Object.prototype.hasOwnProperty,K=function(a){return C(a)?a.toLowerCase():a},vb=function(a){return C(a)?a.toUpperCase():a},wa,x,sb,Ha=[].slice,Kg=[].splice,ph=[].push,la=Object.prototype.toString,Rc=Object.getPrototypeOf,
oa=F("ng"),ca=z.angular||(z.angular={}),lc,qb=0;wa=z.document.documentMode;var Y=Number.isNaN||function(a){return a!==a};E.$inject=[];Ta.$inject=[];var ze=/^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/,V=function(a){return C(a)?a.trim():a},Od=function(a){return a.replace(/([-()[\]{}+?*.$^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")},Ba=function(){if(!w(Ba.rules)){var a=z.document.querySelector("[ng-csp]")||z.document.querySelector("[data-ng-csp]");if(a){var b=
a.getAttribute("ng-csp")||a.getAttribute("data-ng-csp");Ba.rules={noUnsafeEval:!b||-1!==b.indexOf("no-unsafe-eval"),noInlineStyle:!b||-1!==b.indexOf("no-inline-style")}}else{a=Ba;try{new Function(""),b=!1}catch(d){b=!0}a.rules={noUnsafeEval:b,noInlineStyle:!1}}}return Ba.rules},rb=function(){if(w(rb.name_))return rb.name_;var a,b,d=Qa.length,c,e;for(b=0;b<d;++b)if(c=Qa[b],a=z.document.querySelector("["+c.replace(":","\\:")+"jq]")){e=a.getAttribute(c+"jq");break}return rb.name_=e},Be=/:/g,Qa=["ng-",
"data-ng-","ng:","x-ng-"],Fe=function(a){var b=a.currentScript;if(!b)return!0;if(!(b instanceof z.HTMLScriptElement||b instanceof z.SVGScriptElement))return!1;b=b.attributes;return[b.getNamedItem("src"),b.getNamedItem("href"),b.getNamedItem("xlink:href")].every(function(b){if(!b)return!0;if(!b.value)return!1;var c=a.createElement("a");c.href=b.value;if(a.location.origin===c.origin)return!0;switch(c.protocol){case "http:":case "https:":case "ftp:":case "blob:":case "file:":case "data:":return!0;default:return!1}})}(z.document),
Ie=/[A-Z]/g,Yc=!1,Pa=3,Pe={full:"1.8.2",major:1,minor:8,dot:2,codeName:"meteoric-mining"};U.expando="ng339";var Ka=U.cache={},ug=1;U._data=function(a){return this.cache[a[this.expando]]||{}};var qg=/-([a-z])/g,qh=/^-ms-/,Bb={mouseleave:"mouseout",mouseenter:"mouseover"},oc=F("jqLite"),tg=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,nc=/<|&#?\w+;/,rg=/<([\w:-]+)/,sg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,qa={thead:["table"],col:["colgroup","table"],tr:["tbody","table"],td:["tr",
"tbody","table"]};qa.tbody=qa.tfoot=qa.colgroup=qa.caption=qa.thead;qa.th=qa.td;var hb={option:[1,'<select multiple="multiple">',"</select>"],_default:[0,"",""]},Nc;for(Nc in qa){var le=qa[Nc],me=le.slice().reverse();hb[Nc]=[me.length,"<"+me.join("><")+">","</"+le.join("></")+">"]}hb.optgroup=hb.option;var zg=z.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Wa=U.prototype={ready:hd,toString:function(){var a=[];r(this,function(b){a.push(""+b)});return"["+a.join(", ")+
"]"},eq:function(a){return 0<=a?x(this[a]):x(this[this.length+a])},length:0,push:ph,sort:[].sort,splice:[].splice},Hb={};r("multiple selected checked disabled readOnly required open".split(" "),function(a){Hb[K(a)]=a});var od={};r("input select option textarea button form details".split(" "),function(a){od[a]=!0});var vd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern",ngStep:"step"};r({data:sc,removeData:rc,hasData:function(a){for(var b in Ka[a.ng339])return!0;
return!1},cleanData:function(a){for(var b=0,d=a.length;b<d;b++)rc(a[b]),kd(a[b])}},function(a,b){U[b]=a});r({data:sc,inheritedData:Fb,scope:function(a){return x.data(a,"$scope")||Fb(a.parentNode||a,["$isolateScope","$scope"])},isolateScope:function(a){return x.data(a,"$isolateScope")||x.data(a,"$isolateScopeNoTemplate")},controller:ld,injector:function(a){return Fb(a,"$injector")},removeAttr:function(a,b){a.removeAttribute(b)},hasClass:Cb,css:function(a,b,d){b=yb(b.replace(qh,"ms-"));if(w(d))a.style[b]=
d;else return a.style[b]},attr:function(a,b,d){var c=a.nodeType;if(c!==Pa&&2!==c&&8!==c&&a.getAttribute){var c=K(b),e=Hb[c];if(w(d))null===d||!1===d&&e?a.removeAttribute(b):a.setAttribute(b,e?c:d);else return a=a.getAttribute(b),e&&null!==a&&(a=c),null===a?void 0:a}},prop:function(a,b,d){if(w(d))a[b]=d;else return a[b]},text:function(){function a(a,d){if(A(d)){var c=a.nodeType;return 1===c||c===Pa?a.textContent:""}a.textContent=d}a.$dv="";return a}(),val:function(a,b){if(A(b)){if(a.multiple&&"select"===
ua(a)){var d=[];r(a.options,function(a){a.selected&&d.push(a.value||a.text)});return d}return a.value}a.value=b},html:function(a,b){if(A(b))return a.innerHTML;zb(a,!0);a.innerHTML=b},empty:md},function(a,b){U.prototype[b]=function(b,c){var e,f,g=this.length;if(a!==md&&A(2===a.length&&a!==Cb&&a!==ld?b:c)){if(D(b)){for(e=0;e<g;e++)if(a===sc)a(this[e],b);else for(f in b)a(this[e],f,b[f]);return this}e=a.$dv;g=A(e)?Math.min(g,1):g;for(f=0;f<g;f++){var k=a(this[f],b,c);e=e?e+k:k}return e}for(e=0;e<g;e++)a(this[e],
b,c);return this}});r({removeData:rc,on:function(a,b,d,c){if(w(c))throw oc("onargs");if(mc(a)){c=Ab(a,!0);var e=c.events,f=c.handle;f||(f=c.handle=wg(a,e));c=0<=b.indexOf(" ")?b.split(" "):[b];for(var g=c.length,k=function(b,c,g){var k=e[b];k||(k=e[b]=[],k.specialHandlerWrapper=c,"$destroy"===b||g||a.addEventListener(b,f));k.push(d)};g--;)b=c[g],Bb[b]?(k(Bb[b],yg),k(b,void 0,!0)):k(b)}},off:kd,one:function(a,b,d){a=x(a);a.on(b,function e(){a.off(b,d);a.off(b,e)});a.on(b,d)},replaceWith:function(a,
b){var d,c=a.parentNode;zb(a);r(new U(b),function(b){d?c.insertBefore(b,d.nextSibling):c.replaceChild(b,a);d=b})},children:function(a){var b=[];r(a.childNodes,function(a){1===a.nodeType&&b.push(a)});return b},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,b){var d=a.nodeType;if(1===d||11===d){b=new U(b);for(var d=0,c=b.length;d<c;d++)a.appendChild(b[d])}},prepend:function(a,b){if(1===a.nodeType){var d=a.firstChild;r(new U(b),function(b){a.insertBefore(b,d)})}},
wrap:function(a,b){var d=x(b).eq(0).clone()[0],c=a.parentNode;c&&c.replaceChild(d,a);d.appendChild(a)},remove:Gb,detach:function(a){Gb(a,!0)},after:function(a,b){var d=a,c=a.parentNode;if(c){b=new U(b);for(var e=0,f=b.length;e<f;e++){var g=b[e];c.insertBefore(g,d.nextSibling);d=g}}},addClass:Eb,removeClass:Db,toggleClass:function(a,b,d){b&&r(b.split(" "),function(b){var e=d;A(e)&&(e=!Cb(a,b));(e?Eb:Db)(a,b)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},
find:function(a,b){return a.getElementsByTagName?a.getElementsByTagName(b):[]},clone:qc,triggerHandler:function(a,b,d){var c,e,f=b.type||b,g=Ab(a);if(g=(g=g&&g.events)&&g[f])c={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:E,type:f,target:a},b.type&&(c=S(c,
b)),b=ja(g),e=d?[c].concat(d):[c],r(b,function(b){c.isImmediatePropagationStopped()||b.apply(a,e)})}},function(a,b){U.prototype[b]=function(b,c,e){for(var f,g=0,k=this.length;g<k;g++)A(f)?(f=a(this[g],b,c,e),w(f)&&(f=x(f))):pc(f,a(this[g],b,c,e));return w(f)?f:this}});U.prototype.bind=U.prototype.on;U.prototype.unbind=U.prototype.off;var rh=Object.create(null);pd.prototype={_idx:function(a){a!==this._lastKey&&(this._lastKey=a,this._lastIndex=this._keys.indexOf(a));return this._lastIndex},_transformKey:function(a){return Y(a)?
rh:a},get:function(a){a=this._transformKey(a);a=this._idx(a);if(-1!==a)return this._values[a]},has:function(a){a=this._transformKey(a);return-1!==this._idx(a)},set:function(a,b){a=this._transformKey(a);var d=this._idx(a);-1===d&&(d=this._lastIndex=this._keys.length);this._keys[d]=a;this._values[d]=b},delete:function(a){a=this._transformKey(a);a=this._idx(a);if(-1===a)return!1;this._keys.splice(a,1);this._values.splice(a,1);this._lastKey=NaN;this._lastIndex=-1;return!0}};var Ib=pd,og=[function(){this.$get=
[function(){return Ib}]}],Bg=/^([^(]+?)=>/,Cg=/^[^(]*\(\s*([^)]*)\)/m,sh=/,/,th=/^\s*(_?)(\S+?)\1\s*$/,Ag=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ca=F("$injector");fb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw C(d)&&d||(d=a.name||Dg(a)),Ca("strictdi",d);b=qd(a);r(b[1].split(sh),function(a){a.replace(th,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,tb(a[b],"fn"),c=a.slice(0,b)):tb(a,"fn",!0);return c};var ne=F("$animate"),
Ef=function(){this.$get=E},Ff=function(){var a=new Ib,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=C(b)?b.split(" "):H(b)?b:[],r(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){r(b,function(b){var c=a.get(b);if(c){var d=Eg(b.attr("class")),e="",f="";r(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});r(b,function(a){e&&Eb(a,e);f&&Db(a,f)});a.delete(b)}});b.length=0}return{enabled:E,on:E,off:E,pin:E,push:function(g,
k,h,l){l&&l();h=h||{};h.from&&g.css(h.from);h.to&&g.css(h.to);if(h.addClass||h.removeClass)if(k=h.addClass,l=h.removeClass,h=a.get(g)||{},k=e(h,k,!0),l=e(h,l,!1),k||l)a.set(g,h),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},Cf=["$provide",function(a){var b=this,d=null,c=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw ne("notcsel",c);var g=c+"-animation";b.$$registeredAnimations[c.substr(1)]=g;a.factory(g,
d)};this.customFilter=function(a){1===arguments.length&&(c=B(a)?a:null);return c};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,ne("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e<d.length;e++){var f=d[e];if(1===f.nodeType){e=f;break a}}e=void 0}!e||e.parentNode||e.previousElementSibling||(d=null)}d?d.after(a):c.prepend(a)}return{on:a.on,
off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.cancel&&a.cancel()},enter:function(c,d,h,l){d=d&&x(d);h=h&&x(h);d=d||h.parent();b(c,d,h);return a.push(c,"enter",ra(l))},move:function(c,d,h,l){d=d&&x(d);h=h&&x(h);d=d||h.parent();b(c,d,h);return a.push(c,"move",ra(l))},leave:function(b,c){return a.push(b,"leave",ra(c),function(){b.remove()})},addClass:function(b,c,d){d=ra(d);d.addClass=ib(d.addclass,c);return a.push(b,"addClass",d)},removeClass:function(b,c,d){d=ra(d);d.removeClass=ib(d.removeClass,
c);return a.push(b,"removeClass",d)},setClass:function(b,c,d,f){f=ra(f);f.addClass=ib(f.addClass,c);f.removeClass=ib(f.removeClass,d);return a.push(b,"setClass",f)},animate:function(b,c,d,f,m){m=ra(m);m.from=m.from?S(m.from,c):c;m.to=m.to?S(m.to,d):d;m.tempClasses=ib(m.tempClasses,f||"ng-inline-animate");return a.push(b,"animate",m)}}}]}],Hf=function(){this.$get=["$$rAF",function(a){function b(b){d.push(b);1<d.length||a(function(){for(var a=0;a<d.length;a++)d[a]();d=[]})}var d=[];return function(){var a=
!1;b(function(){a=!0});return function(d){a?d():b(d)}}}]},Gf=function(){this.$get=["$q","$sniffer","$$animateAsyncRun","$$isDocumentHidden","$timeout",function(a,b,d,c,e){function f(a){this.setHost(a);var b=d();this._doneCallbacks=[];this._tick=function(a){c()?e(a,0,!1):b(a)};this._state=0}f.chain=function(a,b){function c(){if(d===a.length)b(!0);else a[d](function(a){!1===a?b(!1):(d++,c())})}var d=0;c()};f.all=function(a,b){function c(f){e=e&&f;++d===a.length&&b(e)}var d=0,e=!0;r(a,function(a){a.done(c)})};
f.prototype={setHost:function(a){this.host=a||{}},done:function(a){2===this._state?a():this._doneCallbacks.push(a)},progress:E,getPromise:function(){if(!this.promise){var b=this;this.promise=a(function(a,c){b.done(function(b){!1===b?c():a()})})}return this.promise},then:function(a,b){return this.getPromise().then(a,b)},"catch":function(a){return this.getPromise()["catch"](a)},"finally":function(a){return this.getPromise()["finally"](a)},pause:function(){this.host.pause&&this.host.pause()},resume:function(){this.host.resume&&
this.host.resume()},end:function(){this.host.end&&this.host.end();this._resolve(!0)},cancel:function(){this.host.cancel&&this.host.cancel();this._resolve(!1)},complete:function(a){var b=this;0===b._state&&(b._state=1,b._tick(function(){b._resolve(a)}))},_resolve:function(a){2!==this._state&&(r(this._doneCallbacks,function(b){b(a)}),this._doneCallbacks.length=0,this._state=2)}};return f}]},Df=function(){this.$get=["$$rAF","$q","$$AnimateRunner",function(a,b,d){return function(b,e){function f(){a(function(){g.addClass&&
(b.addClass(g.addClass),g.addClass=null);g.removeClass&&(b.removeClass(g.removeClass),g.removeClass=null);g.to&&(b.css(g.to),g.to=null);k||h.complete();k=!0});return h}var g=e||{};g.$$prepared||(g=Ia(g));g.cleanupStyles&&(g.from=g.to=null);g.from&&(b.css(g.from),g.from=null);var k,h=new d;return{start:f,end:f}}}]},$=F("$compile"),uc=new function(){};Zc.$inject=["$provide","$$sanitizeUriProvider"];Kb.prototype.isFirstChange=function(){return this.previousValue===uc};var rd=/^((?:x|data)[:\-_])/i,Jg=
/[:\-_]+(.)/g,xd=F("$controller"),wd=/^(\S+)(\s+as\s+([\w$]+))?$/,Of=function(){this.$get=["$document",function(a){return function(b){b?!b.nodeType&&b instanceof x&&(b=b[0]):b=a[0].body;return b.offsetWidth+1}}]},yd="application/json",xc={"Content-Type":yd+";charset=utf-8"},Mg=/^\[|^\{(?!\{)/,Ng={"[":/]$/,"{":/}$/},Lg=/^\)]\}',?\n/,Lb=F("$http"),Ma=ca.$interpolateMinErr=F("$interpolate");Ma.throwNoconcat=function(a){throw Ma("noconcat",a);};Ma.interr=function(a,b){return Ma("interr",a,b.toString())};
var Qg=F("$interval"),Xf=function(){this.$get=function(){function a(a){var b=function(a){b.data=a;b.called=!0};b.id=a;return b}var b=ca.callbacks,d={};return{createCallback:function(c){c="_"+(b.$$counter++).toString(36);var e="angular.callbacks."+c,f=a(c);d[e]=b[c]=f;return e},wasCalled:function(a){return d[a].called},getResponse:function(a){return d[a].data},removeCallback:function(a){delete b[d[a].id];delete d[a]}}}},uh=/^([^?#]*)(\?([^#]*))?(#(.*))?$/,Rg={http:80,https:443,ftp:21},kb=F("$location"),
Sg=/^\s*[\\/]{2,}/,vh={$$absUrl:"",$$html5:!1,$$replace:!1,$$compose:function(){for(var a=this.$$path,b=this.$$hash,d=Ce(this.$$search),b=b?"#"+ic(b):"",a=a.split("/"),c=a.length;c--;)a[c]=ic(a[c].replace(/%2F/g,"/"));this.$$url=a.join("/")+(d?"?"+d:"")+b;this.$$absUrl=this.$$normalizeUrl(this.$$url);this.$$urlUpdatedByLocation=!0},absUrl:Mb("$$absUrl"),url:function(a){if(A(a))return this.$$url;var b=uh.exec(a);(b[1]||""===a)&&this.path(decodeURIComponent(b[1]));(b[2]||b[1]||""===a)&&this.search(b[3]||
"");this.hash(b[5]||"");return this},protocol:Mb("$$protocol"),host:Mb("$$host"),port:Mb("$$port"),path:Fd("$$path",function(a){a=null!==a?a.toString():"";return"/"===a.charAt(0)?a:"/"+a}),search:function(a,b){switch(arguments.length){case 0:return this.$$search;case 1:if(C(a)||X(a))a=a.toString(),this.$$search=hc(a);else if(D(a))a=Ia(a,{}),r(a,function(b,c){null==b&&delete a[c]}),this.$$search=a;else throw kb("isrcharg");break;default:A(b)||null===b?delete this.$$search[a]:this.$$search[a]=b}this.$$compose();
return this},hash:Fd("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};r([Ed,Ac,zc],function(a){a.prototype=Object.create(vh);a.prototype.state=function(b){if(!arguments.length)return this.$$state;if(a!==zc||!this.$$html5)throw kb("nostate");this.$$state=A(b)?null:b;this.$$urlUpdatedByLocation=!0;return this}});var Ya=F("$parse"),Wg={}.constructor.prototype.valueOf,Vb=T();r("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Vb[a]=
!0});var wh={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Ob=function(a){this.options=a};Ob.prototype={constructor:Ob,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdentifierStart(this.peekMultichar()))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,
text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var b=a+this.peek(),d=b+this.peek(2),c=Vb[b],e=Vb[d];Vb[a]||c||e?(a=e?d:c?b:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,b){return-1!==b.indexOf(a)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===
typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},
isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=
w(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ya("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index<this.text.length;){var d=K(this.text.charAt(this.index));if("."===d||this.isNumber(d))a+=d;else{var c=this.peek();if("e"===d&&this.isExpOperator(c))a+=d;else if(this.isExpOperator(d)&&c&&this.isNumber(c)&&"e"===a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||c&&this.isNumber(c)||"e"!==a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:b,
text:a,constant:!0,value:Number(a)})},readIdent:function(){var a=this.index;for(this.index+=this.peekMultichar().length;this.index<this.text.length;){var b=this.peekMultichar();if(!this.isIdentifierContinue(b))break;this.index+=b.length}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var b=this.index;this.index++;for(var d="",c=a,e=!1;this.index<this.text.length;){var f=this.text.charAt(this.index),c=c+f;if(e)"u"===f?(e=this.text.substring(this.index+
1,this.index+5),e.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+e+"]"),this.index+=4,d+=String.fromCharCode(parseInt(e,16))):d+=wh[f]||f,e=!1;else if("\\"===f)e=!0;else{if(f===a){this.index++;this.tokens.push({index:b,text:c,constant:!0,value:d});return}d+=f}this.index++}this.throwError("Unterminated quote",b)}};var q=function(a,b){this.lexer=a;this.options=b};q.Program="Program";q.ExpressionStatement="ExpressionStatement";q.AssignmentExpression="AssignmentExpression";q.ConditionalExpression=
"ConditionalExpression";q.LogicalExpression="LogicalExpression";q.BinaryExpression="BinaryExpression";q.UnaryExpression="UnaryExpression";q.CallExpression="CallExpression";q.MemberExpression="MemberExpression";q.Identifier="Identifier";q.Literal="Literal";q.ArrayExpression="ArrayExpression";q.Property="Property";q.ObjectExpression="ObjectExpression";q.ThisExpression="ThisExpression";q.LocalsExpression="LocalsExpression";q.NGValueParameter="NGValueParameter";q.prototype={ast:function(a){this.text=
a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:q.Program,body:a}},expressionStatement:function(){return{type:q.ExpressionStatement,expression:this.filterChain()}},filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},
expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();if(this.expect("=")){if(!Jd(a))throw Ya("lval");a={type:q.AssignmentExpression,left:a,right:this.assignment(),operator:"="}}return a},ternary:function(){var a=this.logicalOR(),b,d;return this.expect("?")&&(b=this.expression(),this.consume(":"))?(d=this.expression(),{type:q.ConditionalExpression,test:a,alternate:b,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a={type:q.LogicalExpression,
operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a={type:q.LogicalExpression,operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),b;b=this.expect("==","!=","===","!==");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.relational()};return a},relational:function(){for(var a=this.additive(),b;b=this.expect("<",">","<=",">=");)a={type:q.BinaryExpression,operator:b.text,
left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:q.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},
primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=Ia(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:q.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",
this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:q.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:q.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:q.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:q.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());
return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:q.Identifier,name:a.text}},constant:function(){return{type:q.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");
return{type:q.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:q.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),
b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");return{type:q.ObjectExpression,properties:a}},throwError:function(a,b){throw Ya("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ya("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ya("ueoe",
this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:q.ThisExpression},$locals:{type:q.LocalsExpression}}};var Hd=2;Ld.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[],
body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};Z(a,b.$filter);var d="",c;this.stage="assign";if(c=Kd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Id(a.body);b.stage="inputs";r(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var k=b.nextId();b.recurse(a,k);b.return_(k);b.state.inputs.push({name:d,isPure:a.isPure});a.watchId=c});this.state.computing="fn";this.stage=
"main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,Tg,Ug,Gd);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;r(b,function(b){a.push("var "+b.name+"="+d.generateFunction(b.name,"s"));b.isPure&&a.push(b.name,".isPure="+JSON.stringify(b.isPure)+
";")});b.length&&a.push("fn.inputs=["+b.map(function(a){return a.name}).join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;r(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},
recurse:function(a,b,d,c,e,f){var g,k,h=this,l,m,p;c=c||E;if(!f&&w(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case q.Program:r(a.body,function(b,c){h.recurse(b.expression,void 0,void 0,function(a){k=a});c!==a.body.length-1?h.current().body.push(k,";"):h.return_(k)});break;case q.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case q.UnaryExpression:this.recurse(a.argument,void 0,
void 0,function(a){k=a});m=a.operator+"("+this.ifDefined(k,0)+")";this.assign(b,m);c(m);break;case q.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){k=a});m="+"===a.operator?this.plus(g,k):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(k,0):"("+g+")"+a.operator+"("+k+")";this.assign(b,m);c(m);break;case q.LogicalExpression:b=b||this.nextId();h.recurse(a.left,b);h.if_("&&"===a.operator?b:h.not(b),h.lazyRecurse(a.right,
b));c(b);break;case q.ConditionalExpression:b=b||this.nextId();h.recurse(a.test,b);h.if_(b,h.lazyRecurse(a.alternate,b),h.lazyRecurse(a.consequent,b));c(b);break;case q.Identifier:b=b||this.nextId();d&&(d.context="inputs"===h.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);h.if_("inputs"===h.stage||h.not(h.getHasOwnProperty("l",a.name)),function(){h.if_("inputs"===h.stage||"s",function(){e&&1!==e&&h.if_(h.isNull(h.nonComputedMember("s",a.name)),
h.lazyAssign(h.nonComputedMember("s",a.name),"{}"));h.assign(b,h.nonComputedMember("s",a.name))})},b&&h.lazyAssign(b,h.nonComputedMember("l",a.name)));c(b);break;case q.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();h.recurse(a.object,g,void 0,function(){h.if_(h.notNull(g),function(){a.computed?(k=h.nextId(),h.recurse(a.property,k),h.getStringValue(k),e&&1!==e&&h.if_(h.not(h.computedMember(g,k)),h.lazyAssign(h.computedMember(g,k),"{}")),m=h.computedMember(g,k),h.assign(b,
m),d&&(d.computed=!0,d.name=k)):(e&&1!==e&&h.if_(h.isNull(h.nonComputedMember(g,a.property.name)),h.lazyAssign(h.nonComputedMember(g,a.property.name),"{}")),m=h.nonComputedMember(g,a.property.name),h.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){h.assign(b,"undefined")});c(b)},!!e);break;case q.CallExpression:b=b||this.nextId();a.filter?(k=h.filter(a.callee.name),l=[],r(a.arguments,function(a){var b=h.nextId();h.recurse(a,b);l.push(b)}),m=k+"("+l.join(",")+")",h.assign(b,m),c(b)):
(k=h.nextId(),g={},l=[],h.recurse(a.callee,k,g,function(){h.if_(h.notNull(k),function(){r(a.arguments,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m=g.name?h.member(g.context,g.name,g.computed)+"("+l.join(",")+")":k+"("+l.join(",")+")";h.assign(b,m)},function(){h.assign(b,"undefined")});c(b)}));break;case q.AssignmentExpression:k=this.nextId();g={};this.recurse(a.left,void 0,g,function(){h.if_(h.notNull(g.context),function(){h.recurse(a.right,k);m=h.member(g.context,
g.name,g.computed)+a.operator+k;h.assign(b,m);c(b||m)})},1);break;case q.ArrayExpression:l=[];r(a.elements,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case q.ObjectExpression:l=[];p=!1;r(a.properties,function(a){a.computed&&(p=!0)});p?(b=b||this.nextId(),this.assign(b,"{}"),r(a.properties,function(a){a.computed?(g=h.nextId(),h.recurse(a.key,g)):g=a.key.type===q.Identifier?a.key.name:""+a.key.value;k=h.nextId();
h.recurse(a.value,k);h.assign(h.member(b,g,a.computed),k)})):(r(a.properties,function(b){h.recurse(b.value,a.constant?void 0:h.nextId(),void 0,function(a){l.push(h.escape(b.key.type===q.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case q.ThisExpression:this.assign(b,"s");c(b||"s");break;case q.LocalsExpression:this.assign(b,"l");c(b||"l");break;case q.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+
b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,
b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,
b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(C(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(X(a))return a.toString();if(!0===a)return"true";if(!1===
a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ya("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Md.prototype={compile:function(a){var b=this;Z(a,b.$filter);var d,c;if(d=Kd(a))c=this.recurse(d);d=Id(a.body);var e;d&&(e=[],r(d,function(a,c){var d=b.recurse(a);d.isPure=a.isPure;a.input=d;e.push(d);a.watchId=c}));var f=[];r(a.body,
function(a){f.push(b.recurse(a.expression))});a=0===a.body.length?E:1===a.body.length?f[0]:function(a,b){var c;r(f,function(d){c=d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case q.Literal:return this.value(a.value,b);case q.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case q.BinaryExpression:return c=this.recurse(a.left),
e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case q.Identifier:return f.identifier(a.name,b,d);case q.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,
e,b,d):this.nonComputedMember(c,e,b,d);case q.CallExpression:return g=[],r(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var p=[],n=0;n<g.length;++n)p.push(g[n](a,c,d,f));a=e.apply(void 0,p,f);return b?{context:void 0,name:void 0,value:a}:a}:function(a,c,d,f){var p=e(a,c,d,f),n;if(null!=p.value){n=[];for(var s=0;s<g.length;++s)n.push(g[s](a,c,d,f));n=p.value.apply(p.context,n)}return b?
{value:n}:n};case q.AssignmentExpression:return c=this.recurse(a.left,!0,1),e=this.recurse(a.right),function(a,d,f,g){var p=c(a,d,f,g);a=e(a,d,f,g);p.context[p.name]=a;return b?{value:a}:a};case q.ArrayExpression:return g=[],r(a.elements,function(a){g.push(f.recurse(a))}),function(a,c,d,e){for(var f=[],n=0;n<g.length;++n)f.push(g[n](a,c,d,e));return b?{value:f}:f};case q.ObjectExpression:return g=[],r(a.properties,function(a){a.computed?g.push({key:f.recurse(a.key),computed:!0,value:f.recurse(a.value)}):
g.push({key:a.key.type===q.Identifier?a.key.name:""+a.key.value,computed:!1,value:f.recurse(a.value)})}),function(a,c,d,e){for(var f={},n=0;n<g.length;++n)g[n].computed?f[g[n].key(a,c,d,e)]=g[n].value(a,c,d,e):f[g[n].key]=g[n].value(a,c,d,e);return b?{value:f}:f};case q.ThisExpression:return function(a){return b?{value:a}:a};case q.LocalsExpression:return function(a,c){return b?{value:c}:c};case q.NGValueParameter:return function(a,c,d){return b?{value:d}:d}}},"unary+":function(a,b){return function(d,
c,e,f){d=a(d,c,e,f);d=w(d)?+d:0;return b?{value:d}:d}},"unary-":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=w(d)?-d:-0;return b?{value:d}:d}},"unary!":function(a,b){return function(d,c,e,f){d=!a(d,c,e,f);return b?{value:d}:d}},"binary+":function(a,b,d){return function(c,e,f,g){var k=a(c,e,f,g);c=b(c,e,f,g);k=Gd(k,c);return d?{value:k}:k}},"binary-":function(a,b,d){return function(c,e,f,g){var k=a(c,e,f,g);c=b(c,e,f,g);k=(w(k)?k:0)-(w(c)?c:0);return d?{value:k}:k}},"binary*":function(a,b,
d){return function(c,e,f,g){c=a(c,e,f,g)*b(c,e,f,g);return d?{value:c}:c}},"binary/":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)/b(c,e,f,g);return d?{value:c}:c}},"binary%":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)%b(c,e,f,g);return d?{value:c}:c}},"binary===":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)===b(c,e,f,g);return d?{value:c}:c}},"binary!==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!==b(c,e,f,g);return d?{value:c}:c}},"binary==":function(a,b,d){return function(c,
e,f,g){c=a(c,e,f,g)==b(c,e,f,g);return d?{value:c}:c}},"binary!=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!=b(c,e,f,g);return d?{value:c}:c}},"binary<":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<b(c,e,f,g);return d?{value:c}:c}},"binary>":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=
a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k)?b(e,f,g,k):d(e,f,g,k);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c=
e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]={});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,k){var h=a(e,f,g,k),l,m;null!=h&&(l=b(e,f,g,k),l+="",c&&1!==c&&h&&!h[l]&&(h[l]={}),m=h[l]);return d?{context:h,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d,
c,e,f){return f?f[b]:a(d,c,e)}}};Nb.prototype={constructor:Nb,parse:function(a){a=this.getAst(a);var b=this.astCompiler.compile(a.ast),d=a.ast;b.literal=0===d.body.length||1===d.body.length&&(d.body[0].expression.type===q.Literal||d.body[0].expression.type===q.ArrayExpression||d.body[0].expression.type===q.ObjectExpression);b.constant=a.ast.constant;b.oneTime=a.oneTime;return b},getAst:function(a){var b=!1;a=a.trim();":"===a.charAt(0)&&":"===a.charAt(1)&&(b=!0,a=a.substring(2));return{ast:this.ast.ast(a),
oneTime:b}}};var Ea=F("$sce"),W={HTML:"html",CSS:"css",MEDIA_URL:"mediaUrl",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},Dc=/_([a-z])/g,Zg=F("$templateRequest"),$g=F("$timeout"),aa=z.document.createElement("a"),Qd=ga(z.location.href),Na;aa.href="http://[::1]";var ah="[::1]"===aa.hostname;Rd.$inject=["$document"];fd.$inject=["$provide"];var Yd=22,Xd=".",Fc="0";Sd.$inject=["$locale"];Ud.$inject=["$locale"];var lh={yyyy:ea("FullYear",4,0,!1,!0),yy:ea("FullYear",2,0,!0,!0),y:ea("FullYear",1,0,!1,!0),
MMMM:lb("Month"),MMM:lb("Month",!0),MM:ea("Month",2,1),M:ea("Month",1,1),LLLL:lb("Month",!1,!0),dd:ea("Date",2),d:ea("Date",1),HH:ea("Hours",2),H:ea("Hours",1),hh:ea("Hours",2,-12),h:ea("Hours",1,-12),mm:ea("Minutes",2),m:ea("Minutes",1),ss:ea("Seconds",2),s:ea("Seconds",1),sss:ea("Milliseconds",3),EEEE:lb("Day"),EEE:lb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Pb(Math[0<a?"floor":"ceil"](a/60),2)+Pb(Math.abs(a%60),2))},
ww:$d(2),w:$d(1),G:Gc,GG:Gc,GGG:Gc,GGGG:function(a,b){return 0>=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},kh=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,jh=/^-?\d+$/;Td.$inject=["$locale"];var eh=ia(K),fh=ia(vb);Vd.$inject=["$parse"];var Re=ia({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===la.call(b.prop("href"))?"xlink:href":"href";
b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),wb={};r(Hb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=xa("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});wb[c]=function(){return{restrict:"A",priority:100,link:e}}}});r(vd,function(a,b){wb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(ke))){e.$set("ngPattern",new RegExp(c[1],
c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});r(["src","srcset","href"],function(a){var b=xa("ng-"+a);wb[b]=["$sce",function(d){return{priority:99,link:function(c,e,f){var g=a,k=a;"href"===a&&"[object SVGAnimatedString]"===la.call(e.prop("href"))&&(k="xlinkHref",f.$attr[k]="xlink:href",g=null);f.$set(b,d.getTrustedMediaUrl(f[b]));f.$observe(b,function(b){b?(f.$set(k,b),wa&&g&&e.prop(g,f[k])):"href"===a&&f.$set(k,null)})}}}]});var mb={$addControl:E,$getControls:ia([]),$$renameControl:function(a,
b){a.$name=b},$removeControl:E,$setValidity:E,$setDirty:E,$setPristine:E,$setSubmitted:E,$$setSubmitted:E};Qb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Qb.prototype={$rollbackViewValue:function(){r(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){r(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ja(a.$name,"input");this.$$controls.push(a);a.$name&&(this[a.$name]=a);a.$$parentForm=this},$getControls:function(){return ja(this.$$controls)},
$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];r(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);r(this.$error,function(b,d){this.$setValidity(d,null,a)},this);r(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);cb(this.$$controls,a);a.$$parentForm=mb},$setDirty:function(){this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element,
Wb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Za,Wb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;r(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){r(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){for(var a=this;a.$$parentForm&&a.$$parentForm!==mb;)a=a.$$parentForm;a.$$setSubmitted()},$$setSubmitted:function(){this.$$animate.addClass(this.$$element,
"ng-submitted");this.$submitted=!0;r(this.$$controls,function(a){a.$$setSubmitted&&a.$$setSubmitted()})}};ce({clazz:Qb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&(cb(c,d),0===c.length&&delete a[b])}});var oe=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||E}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Qb,compile:function(d,f){d.addClass(Za).addClass(nb);
var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var p=f[0];if(!("action"in e)){var n=function(b){a.$apply(function(){p.$commitViewValue();p.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",n);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",n)},0,!1)})}(f[1]||p.$$parentForm).$addControl(p);var s=g?c(p.$name):E;g&&(s(a,p),e.$observe(g,function(b){p.$name!==b&&(s(a,void 0),p.$$parentForm.$$renameControl(p,b),s=c(p.$name),s(a,p))}));
d.on("$destroy",function(){p.$$parentForm.$removeControl(p);s(a,void 0);S(p,mb)})}}}}}]},Se=oe(),df=oe(!0),mh=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,xh=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,yh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/,
nh=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,pe=/^(\d{4,})-(\d{2})-(\d{2})$/,qe=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Oc=/^(\d{4,})-W(\d\d)$/,re=/^(\d{4,})-(\d\d)$/,se=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ee=T();r(["date","datetime-local","month","time","week"],function(a){ee[a]=!0});var te={text:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c)},date:ob("date",pe,Rb(pe,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ob("datetimelocal",qe,Rb(qe,"yyyy MM dd HH mm ss sss".split(" ")),
"yyyy-MM-ddTHH:mm:ss.sss"),time:ob("time",se,Rb(se,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:ob("week",Oc,function(a,b){if(ha(a))return a;if(C(a)){Oc.lastIndex=0;var d=Oc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,k=0,h=Zd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),k=b.getMilliseconds());return new Date(c,0,h.getDate()+e,d,f,g,k)}}return NaN},"yyyy-Www"),month:ob("month",re,Rb(re,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f,g,k){Jc(a,b,d,c,"number");fe(c);Sa(a,
b,d,c,e,f);var h;if(w(d.min)||d.ngMin){var l=d.min||k(d.ngMin)(a);h=na(l);c.$validators.min=function(a,b){return c.$isEmpty(b)||A(h)||b>=h};d.$observe("min",function(a){a!==l&&(h=na(a),l=a,c.$validate())})}if(w(d.max)||d.ngMax){var m=d.max||k(d.ngMax)(a),p=na(m);c.$validators.max=function(a,b){return c.$isEmpty(b)||A(p)||b<=p};d.$observe("max",function(a){a!==m&&(p=na(a),m=a,c.$validate())})}if(w(d.step)||d.ngStep){var n=d.step||k(d.ngStep)(a),s=na(n);c.$validators.step=function(a,b){return c.$isEmpty(b)||
A(s)||ge(b,h||0,s)};d.$observe("step",function(a){a!==n&&(s=na(a),n=a,c.$validate())})}},url:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||xh.test(d)}},email:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||yh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==V(d.ngTrim);A(d.name)&&b.attr("name",++qb);b.on("change",function(a){var g;b[0].checked&&(g=d.value,e&&(g=
V(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=V(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);var e=d[a];d.$observe(a,function(a){a!==e&&(e=a,c(a))})}function k(a){p=na(a);Y(c.$modelValue)||(m?(a=b.val(),p>a&&(a=p,b.val(a)),c.$setViewValue(a)):c.$validate())}function h(a){n=na(a);Y(c.$modelValue)||(m?(a=b.val(),n<a&&(b.val(n),a=n<p?p:n),c.$setViewValue(a)):c.$validate())}function l(a){s=
na(a);Y(c.$modelValue)||(m?c.$viewValue!==b.val()&&c.$setViewValue(b.val()):c.$validate())}Jc(a,b,d,c,"range");fe(c);Sa(a,b,d,c,e,f);var m=c.$$hasNativeValidators&&"range"===b[0].type,p=m?0:void 0,n=m?100:void 0,s=m?1:void 0,r=b[0].validity;a=w(d.min);e=w(d.max);f=w(d.step);var q=c.$render;c.$render=m&&w(r.rangeUnderflow)&&w(r.rangeOverflow)?function(){q();c.$setViewValue(b.val())}:q;a&&(p=na(d.min),c.$validators.min=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||A(p)||b>=p},g("min",k));
e&&(n=na(d.max),c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||A(n)||b<=n},g("max",h));f&&(s=na(d.step),c.$validators.step=m?function(){return!r.stepMismatch}:function(a,b){return c.$isEmpty(b)||A(s)||ge(b,p||0,s)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,k){var h=he(k,a,"ngTrueValue",d.ngTrueValue,!0),l=he(k,a,"ngFalseValue",d.ngFalseValue,!1);b.on("change",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=
function(a){return!1===a};c.$formatters.push(function(a){return va(a,h)});c.$parsers.push(function(a){return a?h:l})},hidden:E,button:E,submit:E,reset:E,file:E},$c=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,k){k[0]&&(te[K(g.type)]||te.text)(e,f,g,k[0],b,a,d,c)}}}}],Af=function(){var a={configurable:!0,enumerable:!1,get:function(){return this.getAttribute("value")||""},set:function(a){this.setAttribute("value",a)}};
return{restrict:"E",priority:200,compile:function(b,d){if("hidden"===K(d.type))return{pre:function(b,d,f,g){b=d[0];b.parentNode&&b.parentNode.insertBefore(b,b.nextSibling);Object.defineProperty&&Object.defineProperty(b,"value",a)}}}}},zh=/^(true|false|\d+)$/,xf=function(){function a(a,d,c){var e=w(c)?c:9===wa?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return zh.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue,
function(b){a(d,f,b)})}}}},We=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=jc(a)})}}}}],Ye=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=A(a)?"":a})}}}}],
Xe=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],wf=ia({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ze=Lc("",!0),af=Lc("Odd",0),$e=Lc("Even",1),bf=Ra({compile:function(a,
b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),cf=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],ed={},Ah={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=xa("ng-"+a);ed[b]=["$parse","$rootScope","$exceptionHandler",function(d,c,e){return sd(d,c,e,b,a,Ah[a])}]});var ff=["$animate","$compile",function(a,b){return{multiElement:!0,
transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var k,h,l;d.$watch(e.ngIf,function(d){d?h||g(function(d,f){h=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);k={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),h&&(h.$destroy(),h=null),k&&(l=ub(k.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),k=null))})}}}],gf=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",
controller:ca.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",k=e.autoscroll;return function(c,e,m,p,n){var r=0,q,t,x,v=function(){t&&(t.remove(),t=null);q&&(q.$destroy(),q=null);x&&(d.leave(x).done(function(a){!1!==a&&(t=null)}),t=x,x=null)};c.$watch(f,function(f){var m=function(a){!1===a||!w(k)||k&&!c.$eval(k)||b()},t=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===r){var b=c.$new();p.template=a;a=n(b,function(a){v();d.enter(a,null,e).done(m)});q=b;x=a;q.$emit("$includeContentLoaded",
f);c.$eval(g)}},function(){c.$$destroyed||t!==r||(v(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(v(),p.template=null)})}}}}],zf=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){la.call(d[0]).match(/SVG/)?(d.empty(),a(gd(e.template,z.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],hf=Ra({priority:450,compile:function(){return{pre:function(a,
b,d){a.$eval(d.ngInit)}}}}),vf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?V(e):e;c.$parsers.push(function(a){if(!A(a)){var b=[];a&&r(a.split(g),function(a){a&&b.push(f?V(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},nb="ng-valid",be="ng-invalid",Za="ng-pristine",Wb="ng-dirty",pb=F("ngModel");Sb.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" ");
Sb.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);B(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){B(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw pb("nonassign",this.$$attr.ngModel,Aa(this.$$element));},$render:E,$isEmpty:function(a){return A(a)||
""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Wb);this.$$animate.addClass(this.$$element,Za)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element,
Za);this.$$animate.addClass(this.$$element,Wb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!Y(this.$modelValue)){var a=
this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;r(h.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(r(h.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;r(h.$asyncValidators,function(e,
g){var h=e(a,b);if(!h||!B(h.then))throw pb("nopromise",h);f(g,void 0);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?h.$$q.all(c).then(function(){g(d)},E):g(!0)}function f(a,b){k===h.$$currentValidationRunId&&h.$setValidity(a,b)}function g(a){k===h.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var k=this.$$currentValidationRunId,h=this;(function(){var a=h.$$parserName;if(A(h.$$parserValid))f(a,null);else return h.$$parserValid||(r(h.$validators,function(a,
b){f(b,null)}),r(h.$asyncValidators,function(a,b){f(b,null)})),f(a,h.$$parserValid),h.$$parserValid;return!0})()?c()?e():g(!1):g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;this.$$parserValid=
A(a)?void 0:!0;this.$setValidity(this.$$parserName,null);this.$$parserName="parse";if(this.$$parserValid)for(var d=0;d<this.$parsers.length;d++)if(a=this.$parsers[d](a),A(a)){this.$$parserValid=!1;break}Y(this.$modelValue)&&(this.$modelValue=this.$$ngModelGet(this.$$scope));var c=this.$modelValue,e=this.$options.getOption("allowInvalid");this.$$rawModelValue=a;e&&(this.$modelValue=a,b.$modelValue!==c&&b.$$writeModelToScope());this.$$runValidators(a,this.$$lastCommittedViewValue,function(d){e||(b.$modelValue=
d?a:void 0,b.$modelValue!==c&&b.$$writeModelToScope())})},$$writeModelToScope:function(){this.$$ngModelSet(this.$$scope,this.$modelValue);r(this.$viewChangeListeners,function(a){try{a()}catch(b){this.$$exceptionHandler(b)}},this)},$setViewValue:function(a,b){this.$viewValue=a;this.$options.getOption("updateOnDefault")&&this.$$debounceViewValueCommit(b)},$$debounceViewValueCommit:function(a){var b=this.$options.getOption("debounce");X(b[a])?b=b[a]:X(b["default"])&&-1===this.$options.getOption("updateOn").indexOf(a)?
b=b["default"]:X(b["*"])&&(b=b["*"]);this.$$timeout.cancel(this.$$pendingDebounce);var d=this;0<b?this.$$pendingDebounce=this.$$timeout(function(){d.$commitViewValue()},b):this.$$rootScope.$$phase?this.$commitViewValue():this.$$scope.$apply(function(){d.$commitViewValue()})},$overrideModelOptions:function(a){this.$options=this.$options.createChild(a);this.$$setUpdateOnEvents()},$processModelValue:function(){var a=this.$$format();this.$viewValue!==a&&(this.$$updateEmptyClasses(a),this.$viewValue=this.$$lastCommittedViewValue=
a,this.$render(),this.$$runValidators(this.$modelValue,this.$viewValue,E))},$$format:function(){for(var a=this.$formatters,b=a.length,d=this.$modelValue;b--;)d=a[b](d);return d},$$setModelValue:function(a){this.$modelValue=this.$$rawModelValue=a;this.$$parserValid=void 0;this.$processModelValue()},$$setUpdateOnEvents:function(){this.$$updateEvents&&this.$$element.off(this.$$updateEvents,this.$$updateEventHandler);if(this.$$updateEvents=this.$options.getOption("updateOn"))this.$$element.on(this.$$updateEvents,
this.$$updateEventHandler)},$$updateEventHandler:function(a){this.$$debounceViewValueCommit(a&&a.type)}};ce({clazz:Sb,set:function(a,b){a[b]=!0},unset:function(a,b){delete a[b]}});var uf=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:Sb,priority:1,compile:function(b){b.addClass(Za).addClass("ng-untouched").addClass(nb);return{pre:function(a,b,e,f){var g=f[0];b=f[1]||g.$$parentForm;if(f=f[2])g.$options=f.$options;g.$$initGetterSetters();b.$addControl(g);
e.$observe("name",function(a){g.$name!==a&&g.$$parentForm.$$renameControl(g,a)});a.$on("$destroy",function(){g.$$parentForm.$removeControl(g)})},post:function(b,c,e,f){function g(){k.$setTouched()}var k=f[0];k.$$setUpdateOnEvents();c.on("blur",function(){k.$touched||(a.$$phase?b.$evalAsync(g):b.$apply(g))})}}}}}],Tb,Bh=/(\s+|^)default(\s+|$)/;Mc.prototype={getOption:function(a){return this.$$options[a]},createChild:function(a){var b=!1;a=S({},a);r(a,function(d,c){"$inherit"===d?"*"===c?b=!0:(a[c]=
this.$$options[c],"updateOn"===c&&(a.updateOnDefault=this.$$options.updateOnDefault)):"updateOn"===c&&(a.updateOnDefault=!1,a[c]=V(d.replace(Bh,function(){a.updateOnDefault=!0;return" "})))},this);b&&(delete a["*"],ie(a,this.$$options));ie(a,Tb.$$options);return new Mc(a)}};Tb=new Mc({updateOn:"",updateOnDefault:!0,debounce:0,getterSetter:!1,allowInvalid:!1,timezone:null});var yf=function(){function a(a,d){this.$$attrs=a;this.$$scope=d}a.$inject=["$attrs","$scope"];a.prototype={$onInit:function(){var a=
this.parentCtrl?this.parentCtrl.$options:Tb,d=this.$$scope.$eval(this.$$attrs.ngModelOptions);this.$options=a.createChild(d)}};return{restrict:"A",priority:10,require:{parentCtrl:"?^^ngModelOptions"},bindToController:!0,controller:a}},jf=Ra({terminal:!0,priority:1E3}),Ch=F("ngOptions"),Dh=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
sf=["$compile","$document","$parse",function(a,b,d){function c(a,b,c){function e(a,b,c,d,f){this.selectValue=a;this.viewValue=b;this.label=c;this.group=d;this.disabled=f}function f(a){var b;if(!r&&za(a))b=a;else{b=[];for(var c in a)a.hasOwnProperty(c)&&"$"!==c.charAt(0)&&b.push(c)}return b}var p=a.match(Dh);if(!p)throw Ch("iexp",a,Aa(b));var n=p[5]||p[7],r=p[6];a=/ as /.test(p[0])&&p[1];var q=p[9];b=d(p[2]?p[1]:n);var t=a&&d(a)||b,w=q&&d(q),v=q?function(a,b){return w(c,b)}:function(a){return La(a)},
x=function(a,b){return v(a,B(a,b))},A=d(p[2]||p[1]),y=d(p[3]||""),J=d(p[4]||""),I=d(p[8]),z={},B=r?function(a,b){z[r]=b;z[n]=a;return z}:function(a){z[n]=a;return z};return{trackBy:q,getTrackByValue:x,getWatchables:d(I,function(a){var b=[];a=a||[];for(var d=f(a),e=d.length,g=0;g<e;g++){var k=a===d?g:d[g],l=a[k],k=B(l,k),l=v(l,k);b.push(l);if(p[2]||p[1])l=A(c,k),b.push(l);p[4]&&(k=J(c,k),b.push(k))}return b}),getOptions:function(){for(var a=[],b={},d=I(c)||[],g=f(d),k=g.length,n=0;n<k;n++){var p=d===
g?n:g[n],r=B(d[p],p),s=t(c,r),p=v(s,r),w=A(c,r),z=y(c,r),r=J(c,r),s=new e(p,s,w,z,r);a.push(s);b[p]=s}return{items:a,selectValueMap:b,getOptionFromViewValue:function(a){return b[x(a)]},getViewValueFromOption:function(a){return q?Ia(a.viewValue):a.viewValue}}}}}var e=z.document.createElement("option"),f=z.document.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","ngModel"],link:{pre:function(a,b,c,d){d[0].registerOption=E},post:function(d,k,h,l){function m(a){var b=(a=v.getOptionFromViewValue(a))&&
a.element;b&&!b.selected&&(b.selected=!0);return a}function p(a,b){a.element=b;b.disabled=a.disabled;a.label!==b.label&&(b.label=a.label,b.textContent=a.label);b.value=a.selectValue}var n=l[0],q=l[1],A=h.multiple;l=0;for(var t=k.children(),z=t.length;l<z;l++)if(""===t[l].value){n.hasEmptyOption=!0;n.emptyOption=t.eq(l);break}k.empty();l=!!n.emptyOption;x(e.cloneNode(!1)).val("?");var v,B=c(h.ngOptions,k,d),C=b[0].createDocumentFragment();n.generateUnknownOptionValue=function(a){return"?"};A?(n.writeValue=
function(a){if(v){var b=a&&a.map(m)||[];v.items.forEach(function(a){a.element.selected&&-1===Array.prototype.indexOf.call(b,a)&&(a.element.selected=!1)})}},n.readValue=function(){var a=k.val()||[],b=[];r(a,function(a){(a=v.selectValueMap[a])&&!a.disabled&&b.push(v.getViewValueFromOption(a))});return b},B.trackBy&&d.$watchCollection(function(){if(H(q.$viewValue))return q.$viewValue.map(function(a){return B.getTrackByValue(a)})},function(){q.$render()})):(n.writeValue=function(a){if(v){var b=k[0].options[k[0].selectedIndex],
c=v.getOptionFromViewValue(a);b&&b.removeAttribute("selected");c?(k[0].value!==c.selectValue&&(n.removeUnknownOption(),k[0].value=c.selectValue,c.element.selected=!0),c.element.setAttribute("selected","selected")):n.selectUnknownOrEmptyOption(a)}},n.readValue=function(){var a=v.selectValueMap[k.val()];return a&&!a.disabled?(n.unselectEmptyOption(),n.removeUnknownOption(),v.getViewValueFromOption(a)):null},B.trackBy&&d.$watch(function(){return B.getTrackByValue(q.$viewValue)},function(){q.$render()}));
l&&(a(n.emptyOption)(d),k.prepend(n.emptyOption),8===n.emptyOption[0].nodeType?(n.hasEmptyOption=!1,n.registerOption=function(a,b){""===b.val()&&(n.hasEmptyOption=!0,n.emptyOption=b,n.emptyOption.removeClass("ng-scope"),q.$render(),b.on("$destroy",function(){var a=n.$isEmptyOptionSelected();n.hasEmptyOption=!1;n.emptyOption=void 0;a&&q.$render()}))}):n.emptyOption.removeClass("ng-scope"));d.$watchCollection(B.getWatchables,function(){var a=v&&n.readValue();if(v)for(var b=v.items.length-1;0<=b;b--){var c=
v.items[b];w(c.group)?Gb(c.element.parentNode):Gb(c.element)}v=B.getOptions();var d={};v.items.forEach(function(a){var b;if(w(a.group)){b=d[a.group];b||(b=f.cloneNode(!1),C.appendChild(b),b.label=null===a.group?"null":a.group,d[a.group]=b);var c=e.cloneNode(!1);b.appendChild(c);p(a,c)}else b=e.cloneNode(!1),C.appendChild(b),p(a,b)});k[0].appendChild(C);q.$render();q.$isEmpty(a)||(b=n.readValue(),(B.trackBy||A?va(a,b):a===b)||(q.$setViewValue(b),q.$render()))})}}}}],kf=["$locale","$interpolate","$log",
function(a,b,d){var c=/{}/g,e=/^when(Minus)?(.+)$/;return{link:function(f,g,k){function h(a){g.text(a||"")}var l=k.count,m=k.$attr.when&&g.attr(k.$attr.when),p=k.offset||0,n=f.$eval(m)||{},q={},w=b.startSymbol(),t=b.endSymbol(),x=w+l+"-"+p+t,v=ca.noop,z;r(k,function(a,b){var c=e.exec(b);c&&(c=(c[1]?"-":"")+K(c[2]),n[c]=g.attr(k.$attr[b]))});r(n,function(a,d){q[d]=b(a.replace(c,x))});f.$watch(l,function(b){var c=parseFloat(b),e=Y(c);e||c in n||(c=a.pluralCat(c-p));c===z||e&&Y(z)||(v(),e=q[c],A(e)?
(null!=b&&d.debug("ngPluralize: no rule defined for '"+c+"' in "+m),v=E,h()):v=f.$watch(e,h),z=c)})}}}],ue=F("ngRef"),lf=["$parse",function(a){return{priority:-1,restrict:"A",compile:function(b,d){var c=xa(ua(b)),e=a(d.ngRef),f=e.assign||function(){throw ue("nonassign",d.ngRef);};return function(a,b,h){var l;if(h.hasOwnProperty("ngRefRead"))if("$element"===h.ngRefRead)l=b;else{if(l=b.data("$"+h.ngRefRead+"Controller"),!l)throw ue("noctrl",h.ngRefRead,d.ngRef);}else l=b.data("$"+c+"Controller");l=
l||b;f(a,l);b.on("$destroy",function(){e(a)===l&&f(a,null)})}}}}],mf=["$parse","$animate","$compile",function(a,b,d){var c=F("ngRepeat"),e=function(a,b,c,d,e,f,g){a[c]=d;e&&(a[e]=f);a.$index=b;a.$first=0===b;a.$last=b===g-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(b&1))},f=function(a,b,c){return La(c)},g=function(a,b){return b};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(k,h){var l=h.ngRepeat,m=d.$$createComment("end ngRepeat",
l),p=l.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!p)throw c("iexp",l);var n=p[1],q=p[2],w=p[3],t=p[4],p=n.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);if(!p)throw c("iidexp",n);var x=p[3]||p[1],v=p[2];if(w&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(w)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(w)))throw c("badident",w);var A;if(t){var z={$id:La},y=a(t);A=function(a,b,c,d){v&&
(z[v]=b);z[x]=c;z.$index=d;return y(a,z)}}return function(a,d,h,k,n){var p=T();a.$watchCollection(q,function(h){var k,q,t=d[0],s,y=T(),B,C,E,D,H,F,K;w&&(a[w]=h);if(za(h))H=h,q=A||f;else for(K in q=A||g,H=[],h)ta.call(h,K)&&"$"!==K.charAt(0)&&H.push(K);B=H.length;K=Array(B);for(k=0;k<B;k++)if(C=h===H?k:H[k],E=h[C],D=q(a,C,E,k),p[D])F=p[D],delete p[D],y[D]=F,K[k]=F;else{if(y[D])throw r(K,function(a){a&&a.scope&&(p[a.id]=a)}),c("dupes",l,D,E);K[k]={id:D,scope:void 0,clone:void 0};y[D]=!0}z&&(z[x]=void 0);
for(s in p){F=p[s];D=ub(F.clone);b.leave(D);if(D[0].parentNode)for(k=0,q=D.length;k<q;k++)D[k].$$NG_REMOVED=!0;F.scope.$destroy()}for(k=0;k<B;k++)if(C=h===H?k:H[k],E=h[C],F=K[k],F.scope){s=t;do s=s.nextSibling;while(s&&s.$$NG_REMOVED);F.clone[0]!==s&&b.move(ub(F.clone),null,t);t=F.clone[F.clone.length-1];e(F.scope,k,x,E,v,C,B)}else n(function(a,c){F.scope=c;var d=m.cloneNode(!1);a[a.length++]=d;b.enter(a,null,t);t=d;F.clone=a;y[F.id]=F;e(F.scope,k,x,E,v,C,B)});p=y})}}}}],nf=["$animate",function(a){return{restrict:"A",
multiElement:!0,link:function(b,d,c){b.$watch(c.ngShow,function(b){a[b?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],ef=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngHide,function(b){a[b?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],of=Ra(function(a,b,d){a.$watchCollection(d.ngStyle,function(a,d){d&&a!==d&&r(d,function(a,c){b.css(c,"")});a&&b.css(a)})}),pf=["$animate","$compile",function(a,
b){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(d,c,e,f){var g=[],k=[],h=[],l=[],m=function(a,b){return function(c){!1!==c&&a.splice(b,1)}};d.$watch(e.ngSwitch||e.on,function(c){for(var d,e;h.length;)a.cancel(h.pop());d=0;for(e=l.length;d<e;++d){var q=ub(k[d].clone);l[d].$destroy();(h[d]=a.leave(q)).done(m(h,d))}k.length=0;l.length=0;(g=f.cases["!"+c]||f.cases["?"])&&r(g,function(c){c.transclude(function(d,e){l.push(e);var f=c.element;d[d.length++]=b.$$createComment("end ngSwitchWhen");
k.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],qf=Ra({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){a=d.ngSwitchWhen.split(d.ngSwitchWhenSeparator).sort().filter(function(a,b,c){return c[b-1]!==a});r(a,function(a){c.cases["!"+a]=c.cases["!"+a]||[];c.cases["!"+a].push({transclude:e,element:b})})}}),rf=Ra({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["?"]=c.cases["?"]||[];c.cases["?"].push({transclude:e,
element:b})}}),Eh=F("ngTransclude"),tf=["$compile",function(a){return{restrict:"EAC",compile:function(b){var d=a(b.contents());b.empty();return function(a,b,f,g,k){function h(){d(a,function(a){b.append(a)})}if(!k)throw Eh("orphan",Aa(b));f.ngTransclude===f.$attr.ngTransclude&&(f.ngTransclude="");f=f.ngTransclude||f.ngTranscludeSlot;k(function(a,c){var d;if(d=a.length)a:{d=0;for(var f=a.length;d<f;d++){var g=a[d];if(g.nodeType!==Pa||g.nodeValue.trim()){d=!0;break a}}d=void 0}d?b.append(a):(h(),c.$destroy())},
null,f);f&&!k.isSlotFilled(f)&&h()}}}}],Te=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(b,d){"text/ng-template"===d.type&&a.put(d.id,b[0].text)}}}],Fh={$setViewValue:E,$render:E},Gh=["$element","$scope",function(a,b){function d(){g||(g=!0,b.$$postDigest(function(){g=!1;e.ngModelCtrl.$render()}))}function c(a){k||(k=!0,b.$$postDigest(function(){b.$$destroyed||(k=!1,e.ngModelCtrl.$setViewValue(e.readValue()),a&&e.ngModelCtrl.$render())}))}var e=this,f=new Ib;e.selectValueMap=
{};e.ngModelCtrl=Fh;e.multiple=!1;e.unknownOption=x(z.document.createElement("option"));e.hasEmptyOption=!1;e.emptyOption=void 0;e.renderUnknownOption=function(b){b=e.generateUnknownOptionValue(b);e.unknownOption.val(b);a.prepend(e.unknownOption);Oa(e.unknownOption,!0);a.val(b)};e.updateUnknownOption=function(b){b=e.generateUnknownOptionValue(b);e.unknownOption.val(b);Oa(e.unknownOption,!0);a.val(b)};e.generateUnknownOptionValue=function(a){return"? "+La(a)+" ?"};e.removeUnknownOption=function(){e.unknownOption.parent()&&
e.unknownOption.remove()};e.selectEmptyOption=function(){e.emptyOption&&(a.val(""),Oa(e.emptyOption,!0))};e.unselectEmptyOption=function(){e.hasEmptyOption&&Oa(e.emptyOption,!1)};b.$on("$destroy",function(){e.renderUnknownOption=E});e.readValue=function(){var b=a.val(),b=b in e.selectValueMap?e.selectValueMap[b]:b;return e.hasOption(b)?b:null};e.writeValue=function(b){var c=a[0].options[a[0].selectedIndex];c&&Oa(x(c),!1);e.hasOption(b)?(e.removeUnknownOption(),c=La(b),a.val(c in e.selectValueMap?
c:b),Oa(x(a[0].options[a[0].selectedIndex]),!0)):e.selectUnknownOrEmptyOption(b)};e.addOption=function(a,b){if(8!==b[0].nodeType){Ja(a,'"option value"');""===a&&(e.hasEmptyOption=!0,e.emptyOption=b);var c=f.get(a)||0;f.set(a,c+1);d()}};e.removeOption=function(a){var b=f.get(a);b&&(1===b?(f.delete(a),""===a&&(e.hasEmptyOption=!1,e.emptyOption=void 0)):f.set(a,b-1))};e.hasOption=function(a){return!!f.get(a)};e.$hasEmptyOption=function(){return e.hasEmptyOption};e.$isUnknownOptionSelected=function(){return a[0].options[0]===
e.unknownOption[0]};e.$isEmptyOptionSelected=function(){return e.hasEmptyOption&&a[0].options[a[0].selectedIndex]===e.emptyOption[0]};e.selectUnknownOrEmptyOption=function(a){null==a&&e.emptyOption?(e.removeUnknownOption(),e.selectEmptyOption()):e.unknownOption.parent().length?e.updateUnknownOption(a):e.renderUnknownOption(a)};var g=!1,k=!1;e.registerOption=function(a,b,f,g,k){if(f.$attr.ngValue){var q,r;f.$observe("value",function(a){var d,f=b.prop("selected");w(r)&&(e.removeOption(q),delete e.selectValueMap[r],
d=!0);r=La(a);q=a;e.selectValueMap[r]=a;e.addOption(a,b);b.attr("value",r);d&&f&&c()})}else g?f.$observe("value",function(a){e.readValue();var d,f=b.prop("selected");w(q)&&(e.removeOption(q),d=!0);q=a;e.addOption(a,b);d&&f&&c()}):k?a.$watch(k,function(a,d){f.$set("value",a);var g=b.prop("selected");d!==a&&e.removeOption(d);e.addOption(a,b);d&&g&&c()}):e.addOption(f.value,b);f.$observe("disabled",function(a){if("true"===a||a&&b.prop("selected"))e.multiple?c(!0):(e.ngModelCtrl.$setViewValue(null),e.ngModelCtrl.$render())});
b.on("$destroy",function(){var a=e.readValue(),b=f.value;e.removeOption(b);d();(e.multiple&&a&&-1!==a.indexOf(b)||a===b)&&c(!0)})}}],Ue=function(){return{restrict:"E",require:["select","?ngModel"],controller:Gh,priority:1,link:{pre:function(a,b,d,c){var e=c[0],f=c[1];if(f){if(e.ngModelCtrl=f,b.on("change",function(){e.removeUnknownOption();a.$apply(function(){f.$setViewValue(e.readValue())})}),d.multiple){e.multiple=!0;e.readValue=function(){var a=[];r(b.find("option"),function(b){b.selected&&!b.disabled&&
(b=b.value,a.push(b in e.selectValueMap?e.selectValueMap[b]:b))});return a};e.writeValue=function(a){r(b.find("option"),function(b){var c=!!a&&(-1!==Array.prototype.indexOf.call(a,b.value)||-1!==Array.prototype.indexOf.call(a,e.selectValueMap[b.value]));c!==b.selected&&Oa(x(b),c)})};var g,k=NaN;a.$watch(function(){k!==f.$viewValue||va(g,f.$viewValue)||(g=ja(f.$viewValue),f.$render());k=f.$viewValue});f.$isEmpty=function(a){return!a||0===a.length}}}else e.registerOption=E},post:function(a,b,d,c){var e=
c[1];if(e){var f=c[0];e.$render=function(){f.writeValue(e.$viewValue)}}}}}},Ve=["$interpolate",function(a){return{restrict:"E",priority:100,compile:function(b,d){var c,e;w(d.ngValue)||(w(d.value)?c=a(d.value,!0):(e=a(b.text(),!0))||d.$set("value",b.text()));return function(a,b,d){var h=b.parent();(h=h.data("$selectController")||h.parent().data("$selectController"))&&h.registerOption(a,b,d,c,e)}}}}],bd=["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=
c.hasOwnProperty("required")||a(c.ngRequired)(b);c.ngRequired||(c.required=!0);e.$validators.required=function(a,b){return!f||!e.$isEmpty(b)};c.$observe("required",function(a){f!==a&&(f=a,e.$validate())})}}}}],ad=["$parse",function(a){return{restrict:"A",require:"?ngModel",compile:function(b,d){var c,e;d.ngPattern&&(c=d.ngPattern,e="/"===d.ngPattern.charAt(0)&&ke.test(d.ngPattern)?function(){return d.ngPattern}:a(d.ngPattern));return function(a,b,d,h){if(h){var l=d.pattern;d.ngPattern?l=e(a):c=d.pattern;
var m=je(l,c,b);d.$observe("pattern",function(a){var d=m;m=je(a,c,b);(d&&d.toString())!==(m&&m.toString())&&h.$validate()});h.$validators.pattern=function(a,b){return h.$isEmpty(b)||A(m)||m.test(b)}}}}}}],dd=["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.maxlength||a(c.ngMaxlength)(b),g=Ub(f);c.$observe("maxlength",function(a){f!==a&&(g=Ub(a),f=a,e.$validate())});e.$validators.maxlength=function(a,b){return 0>g||e.$isEmpty(b)||b.length<=g}}}}}],cd=
["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.minlength||a(c.ngMinlength)(b),g=Ub(f)||-1;c.$observe("minlength",function(a){f!==a&&(g=Ub(a)||-1,f=a,e.$validate())});e.$validators.minlength=function(a,b){return e.$isEmpty(b)||b.length>=g}}}}}];z.angular.bootstrap?z.console&&console.log("WARNING: Tried to load AngularJS more than once."):(Je(),Oe(ca),ca.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==
b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),
WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a,
c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),x(function(){Ee(z.document,Wc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(window.angular.element("<style>").text('@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}'));
//# sourceMappingURL=angular.min.js.map
-7
View File
@@ -1,7 +0,0 @@
/**
* angular-ui-ace - This directive allows you to add ACE editor elements.
* @version v0.2.3 - 2015-01-29
* @link http://angular-ui.github.com
* @license MIT
*/
"use strict";angular.module("ui.ace",[]).constant("uiAceConfig",{}).directive("uiAce",["uiAceConfig",function(a){if(angular.isUndefined(window.ace))throw new Error("ui-ace need ace to work... (o rly?)");var b=function(a,b,c){if(angular.isDefined(c.workerPath)){var d=window.ace.require("ace/config");d.set("workerPath",c.workerPath)}angular.isDefined(c.require)&&c.require.forEach(function(a){window.ace.require(a)}),angular.isDefined(c.showGutter)&&a.renderer.setShowGutter(c.showGutter),angular.isDefined(c.useWrapMode)&&b.setUseWrapMode(c.useWrapMode),angular.isDefined(c.showInvisibles)&&a.renderer.setShowInvisibles(c.showInvisibles),angular.isDefined(c.showIndentGuides)&&a.renderer.setDisplayIndentGuides(c.showIndentGuides),angular.isDefined(c.useSoftTabs)&&b.setUseSoftTabs(c.useSoftTabs),angular.isDefined(c.showPrintMargin)&&a.setShowPrintMargin(c.showPrintMargin),angular.isDefined(c.disableSearch)&&c.disableSearch&&a.commands.addCommands([{name:"unfind",bindKey:{win:"Ctrl-F",mac:"Command-F"},exec:function(){return!1},readOnly:!0}]),angular.isString(c.theme)&&a.setTheme("ace/theme/"+c.theme),angular.isString(c.mode)&&b.setMode("ace/mode/"+c.mode),angular.isDefined(c.firstLineNumber)&&(angular.isNumber(c.firstLineNumber)?b.setOption("firstLineNumber",c.firstLineNumber):angular.isFunction(c.firstLineNumber)&&b.setOption("firstLineNumber",c.firstLineNumber()));var e,f;if(angular.isDefined(c.advanced))for(e in c.advanced)f={name:e,value:c.advanced[e]},a.setOption(f.name,f.value);if(angular.isDefined(c.rendererOptions))for(e in c.rendererOptions)f={name:e,value:c.rendererOptions[e]},a.renderer.setOption(f.name,f.value);angular.forEach(c.callbacks,function(b){angular.isFunction(b)&&b(a)})};return{restrict:"EA",require:"?ngModel",link:function(c,d,e,f){var g,h,i=a.ace||{},j=angular.extend({},i,c.$eval(e.uiAce)),k=window.ace.edit(d[0]),l=k.getSession(),m=function(){var a=arguments[0],b=Array.prototype.slice.call(arguments,1);angular.isDefined(a)&&c.$evalAsync(function(){if(!angular.isFunction(a))throw new Error("ui-ace use a function as callback.");a(b)})},n={onChange:function(a){return function(b){var d=l.getValue();!f||d===f.$viewValue||c.$$phase||c.$root.$$phase||c.$evalAsync(function(){f.$setViewValue(d)}),m(a,b,k)}},onBlur:function(a){return function(){m(a,k)}}};e.$observe("readonly",function(a){k.setReadOnly(!!a||""===a)}),f&&(f.$formatters.push(function(a){if(angular.isUndefined(a)||null===a)return"";if(angular.isObject(a)||angular.isArray(a))throw new Error("ui-ace cannot use an object or an array as a model");return a}),f.$render=function(){l.setValue(f.$viewValue)});var o=function(a,d){a!==d&&(j=angular.extend({},i,c.$eval(e.uiAce)),j.callbacks=[j.onLoad],j.onLoad!==i.onLoad&&j.callbacks.unshift(i.onLoad),l.removeListener("change",g),g=n.onChange(j.onChange),l.on("change",g),k.removeListener("blur",h),h=n.onBlur(j.onBlur),k.on("blur",h),b(k,l,j))};c.$watch(e.uiAce,o,!0),o(i),d.on("$destroy",function(){k.session.$stopWorker(),k.destroy()}),c.$watch(function(){return[d[0].offsetWidth,d[0].offsetHeight]},function(){k.resize(),k.renderer.updateFull()},!0)}}}]);
+299
View File
@@ -0,0 +1,299 @@
export const humanFileSize = value => window.humanFileSize(value);
export const 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";
};
})();
export const 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. Spell the month out so the date
// is unambiguous regardless of the reader's locale (9/6 vs 6/9).
if (Math.abs(seconds) > 2 * 60 * 60 * 24) {
const now = new Date();
now.setSeconds(now.getSeconds() - seconds);
return (
"on " +
now.toLocaleDateString(undefined, {
day: "numeric",
month: "short",
year: "numeric",
})
);
}
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;
};
})();
export const 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(" ");
};
})();
export const statusLabel = (function () {
var labels = {
ready: "Ready",
error: "Error",
expired: "Expired",
expiring: "Expiring",
removed: "Removed",
removing: "Removing",
queue: "Queued",
download: "Downloading",
downloaded: "Downloaded",
preparing: "Preparing",
anonymizing: "Anonymizing",
};
return function (status) {
if (!status) return "";
if (labels[status]) return labels[status];
var s = String(status).replace(/[_-]+/g, " ").toLowerCase();
return s.charAt(0).toUpperCase() + s.slice(1);
};
})();
export const statusMsg = (function () {
// Known machine codes → sentences. Unknown snake_case codes are
// converted to a sentence instead of leaking `branch_not_found`.
var codes = {
branch_not_found: "Branch not found on GitHub",
repo_not_found: "Repository not found on GitHub",
repository_not_found: "Repository not found on GitHub",
repo_not_accessible: "Repository is not accessible with your token",
pr_not_found: "Pull request not found on GitHub",
gist_not_found: "Gist not found on GitHub",
commit_not_found: "Commit not found on GitHub",
repo_too_big: "Repository exceeds the size limit",
quota_exceeded: "Storage quota exceeded",
incomplete_record: "Incomplete record: missing identifier",
};
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");
}
if (codes[msg]) return codes[msg];
if (/^[a-z0-9]+(_[a-z0-9]+)+$/.test(msg)) {
var s = msg.replace(/_/g, " ");
return s.charAt(0).toUpperCase() + s.slice(1);
}
return msg;
};
})();
export const diff = (function (html) {
const esc = (s) =>
s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 out.join("");
};
})({ trustAsHtml: value => value });
export function number(value, digits) {
return value == null ? "" : Number(value).toLocaleString(undefined, digits == null ? {} : { minimumFractionDigits: digits, maximumFractionDigits: digits });
}
export function uppercase(value) { return String(value ?? "").toUpperCase(); }
export function limitTo(value, count, start = 0) { return (value || []).slice(start, start + count); }
export function filter(values, predicate) {
if (!Array.isArray(values)) return [];
if (typeof predicate === "function") return values.filter(predicate);
return values;
}
export function orderBy(values, expression) {
if (!expression) return values || [];
const keys = Array.isArray(expression) ? expression : [expression];
return [...(values || [])].sort((a, b) => {
for (let key of keys) {
const direction = key[0] === "-" ? -1 : 1;
key = key.replace(/^[+-]/, "");
let av = key.split(".").reduce((o, p) => o?.[p], a);
let bv = key.split(".").reduce((o, p) => o?.[p], b);
if (typeof av === "string") av = av.toLowerCase();
if (typeof bv === "string") bv = bv.toLowerCase();
if (av !== bv) return (av == null ? -1 : bv == null ? 1 : av < bv ? -1 : 1) * direction;
}
return 0;
});
}
export function date(value, format) {
if (!value) return "";
const d = new Date(value);
if (isNaN(d)) return String(value);
if (!format || format === "mediumDate") return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
const parts = { yyyy: d.getFullYear(), MMM: d.toLocaleDateString(undefined, { month: "short" }), MM: String(d.getMonth() + 1).padStart(2, "0"), dd: String(d.getDate()).padStart(2, "0"), d: d.getDate(), HH: String(d.getHours()).padStart(2, "0"), mm: String(d.getMinutes()).padStart(2, "0"), ss: String(d.getSeconds()).padStart(2, "0") };
return format.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g, token => parts[token]);
}
export function plural(count, forms) {
return (forms?.[count] ?? forms?.[count === 1 ? "one" : "other"] ?? "").replace(/\{\}/g, count ?? 0);
}
+96
View File
@@ -0,0 +1,96 @@
import { reactive } from "vue";
function validationState() {
return reactive({
errors: {}, dirty: false, touched: false, submitted: false,
get invalid() { return Object.values(this.errors).some(Boolean); },
setValidity(key, valid) { this.errors[key] = !valid; },
setDirty() { this.dirty = true; },
});
}
function ensureForm(state, name) {
if (!state[name]) {
const form = validationState();
Object.defineProperty(form, "invalid", { get() {
return Object.values(form.errors).some(Boolean) || Object.keys(form).some(key => key !== "invalid" && form[key] && typeof form[key] === "object" && form[key].invalid);
} });
state[name] = form;
}
return state[name];
}
export const formDirective = {
beforeMount(el, { value: state }) {
el._formState = ensureForm(state, el.name);
el.addEventListener("submit", event => event.preventDefault());
},
};
function dateInput(value) {
if (!(value instanceof Date)) return value || "";
if (isNaN(value)) return "";
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
}
function sync(el) {
const value = el._field.binding.value;
if (el.type === "checkbox") el.checked = !!value;
else if (el.type === "radio") el.checked = value == (el._value ?? el.value);
else if (!el._field.timer) el.value = el.type === "date" ? dateInput(value) : value ?? "";
validate(el);
}
function validate(el) {
const field = el._field.validation;
if (!field) return;
const validity = el.validity;
for (const [key, invalid] of Object.entries({ required: validity.valueMissing, pattern: validity.patternMismatch, min: validity.rangeUnderflow, max: validity.rangeOverflow, date: validity.badInput, email: validity.typeMismatch && el.type === "email", url: validity.typeMismatch && el.type === "url" })) {
field.setValidity(key, el.disabled || !invalid);
}
}
export const fieldDirective = {
beforeMount(el, { value: binding }) {
const form = binding.form && ensureForm(binding.state, binding.form);
const validation = form && el.name ? (form[el.name] ||= validationState()) : null;
el._field = { binding, validation, timer: null };
const commit = () => {
clearTimeout(el._field.timer); el._field.timer = null;
const binding = el._field.binding;
let value = el.type === "checkbox" ? el.checked : el._value ?? el.value;
if (el.type === "radio" && !el.checked) return;
if (el.type === "number" || el.type === "range") value = el.value === "" ? null : Number(el.value);
if (el.type === "date") value = el.value ? new Date(el.value + "T00:00:00") : null;
if (validation) {
validation.setDirty();
for (const key of Object.keys(validation.errors)) validation.setValidity(key, true);
}
binding.set(value);
validate(el);
binding.change?.();
};
el._field.commit = commit;
const update = event => {
const debounce = el._field.binding.options?.debounce || 0;
const delay = typeof debounce === "number" ? debounce : debounce[event.type] ?? debounce.default ?? 0;
clearTimeout(el._field.timer);
if (delay) el._field.timer = setTimeout(commit, delay);
else commit();
};
el.addEventListener(el.tagName === "SELECT" || ["checkbox", "radio", "date"].includes(el.type) ? "change" : "input", update);
el.addEventListener("blur", () => { if (validation) validation.touched = true; if (el._field.timer) commit(); });
},
mounted: sync,
updated(el, { value }) { el._field.binding = value; sync(el); },
beforeUnmount(el) {
clearTimeout(el._field.timer);
const { binding, validation } = el._field;
const form = binding.form && binding.state[binding.form];
if (form && form[el.name] === validation) delete form[el.name];
},
};
export function submitForm(event, callback) {
const form = event.target.closest("form");
if (form?._formState) form._formState.submitted = true;
form?.querySelectorAll("input, select, textarea").forEach(el => {
if (el._field?.timer) el._field.commit();
if (el._field) validate(el);
});
if (form && !form.checkValidity()) return;
return callback();
}
+21 -40
View File
@@ -1,33 +1,13 @@
/** import { h, ref, watch, onMounted } from "vue";
* Renders a repository's .html file as a document instead of showing its
* source (#771). Self-contained HTML reports Quarto, R Markdown, nbconvert export default {
* are a common way to ship rendered results, and dumping several megabytes name: "htmlDoc",
* of markup into the code editor was neither readable nor fast. props: ["content","baseUrl","allowScripts"],
* setup(props) {
* The markup is untrusted: it comes from the repository under review. It is const elementRef = ref(null);
* therefore written into a `sandbox`ed iframe, and scripts are OFF by default onMounted(() => {
* the reader opts in per file with the "Enable JS" action. Even when they const element = [elementRef.value];
* do, the sandbox never gets `allow-same-origin`: with `allow-scripts` that
* pair lets the framed document remove its own sandbox, after which it could
* reach the app's cookies, session and DOM.
*
* We use `srcdoc` rather than pointing the iframe at the file API so the
* response's `X-Frame-Options: SAMEORIGIN` doesn't block the frame: under
* sandbox the document's origin is opaque and never matches "same origin".
* An injected <base> keeps any relative images/stylesheets resolving against
* the file's own directory in the anonymized repo.
*/
angular.module("htmlDoc", []).directive("htmlDoc", [
function () {
return {
restrict: "E",
scope: {
content: "<",
baseUrl: "@",
allowScripts: "<",
},
link: function (scope, element) {
const host = element[0]; const host = element[0];
host.classList.add("html-doc"); host.classList.add("html-doc");
@@ -35,15 +15,15 @@ angular.module("htmlDoc", []).directive("htmlDoc", [
// The sandbox attribute only takes effect on navigation, so a fresh // The sandbox attribute only takes effect on navigation, so a fresh
// iframe is the reliable way to apply a changed policy. // iframe is the reliable way to apply a changed policy.
host.innerHTML = ""; host.innerHTML = "";
const content = scope.content; const content = props.content;
if (typeof content !== "string") return; if (typeof content !== "string") return;
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.className = "html-doc-frame"; iframe.className = "html-doc-frame";
iframe.setAttribute("title", "Rendered HTML document"); iframe.setAttribute("title", "Rendered HTML document");
// No allow-same-origin — see the note above. // Never combine allow-scripts with allow-same-origin.
const sandbox = ["allow-popups", "allow-popups-to-escape-sandbox"]; const sandbox = ["allow-popups", "allow-popups-to-escape-sandbox"];
if (scope.allowScripts) { if (props.allowScripts) {
sandbox.push("allow-scripts", "allow-forms", "allow-modals"); sandbox.push("allow-scripts", "allow-forms", "allow-modals");
} }
iframe.setAttribute("sandbox", sandbox.join(" ")); iframe.setAttribute("sandbox", sandbox.join(" "));
@@ -51,19 +31,20 @@ angular.module("htmlDoc", []).directive("htmlDoc", [
host.appendChild(iframe); host.appendChild(iframe);
let base = ""; let base = "";
if (scope.baseUrl) { if (props.baseUrl) {
base = base =
'<base href="' + '<base href="' +
scope.baseUrl.replace(/&/g, "&amp;").replace(/"/g, "&quot;") + props.baseUrl.replace(/&/g, "&amp;").replace(/"/g, "&quot;") +
'">'; '">';
} }
iframe.srcdoc = base + content; iframe.srcdoc = base + content;
} }
scope.$watch("content", render); watch(() => props.content, render, { immediate: true });
scope.$watch("baseUrl", render); watch(() => props.baseUrl, render, { immediate: true });
scope.$watch("allowScripts", render); watch(() => props.allowScripts, render, { immediate: true });
});
return () => h("html-doc", { ref: elementRef });
}, },
}; };
},
]);
+52
View File
@@ -0,0 +1,52 @@
// Keep response metadata alongside parsed data for the existing API consumers.
export function createHttp(fetcher = (...args) => fetch(...args)) {
async function request(method, url, data, options = {}) {
const target = new URL(url, window.location.href);
for (const [key, value] of Object.entries(options.params || {})) {
if (value == null) continue;
for (const item of Array.isArray(value) ? value : [value]) target.searchParams.append(key, typeof item === "object" ? JSON.stringify(item) : item);
}
const controller = new AbortController();
let timer;
if (options.timeout?.then) options.timeout.then(() => controller.abort());
else if (options.timeout) timer = setTimeout(() => controller.abort(), options.timeout);
try {
const headers = { Accept: "application/json, text/plain, */*", ...options.headers };
const csrf = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/);
if (csrf && target.origin === window.location.origin) headers["X-XSRF-TOKEN"] = decodeURIComponent(csrf[1]);
let body;
if (data !== undefined) {
if (data instanceof FormData) body = data;
else { headers["Content-Type"] ||= "application/json;charset=utf-8"; body = JSON.stringify(data); }
}
const response = await fetcher(target.href, { method, headers, body, credentials: "same-origin", signal: controller.signal });
const raw = await response.text();
let result = raw;
if (options.transformResponse) result = options.transformResponse(raw);
else if (raw && (/json/i.test(response.headers.get("content-type") || "") || /^[\[{]/.test(raw.trim()))) {
try { result = JSON.parse(raw); } catch { /* File content may start with a brace. */ }
}
const value = { data: result, status: response.status, headers: name => response.headers.get(name) };
if (!response.ok) throw value;
return value;
} catch (error) {
if (error.name === "AbortError") throw { status: -1, data: null };
throw error;
} finally { clearTimeout(timer); }
}
const http = {};
for (const method of ["get", "delete", "head"]) http[method] = (url, options) => request(method.toUpperCase(), url, undefined, options);
for (const method of ["post", "put", "patch"]) http[method] = (url, data, options) => request(method.toUpperCase(), url, data, options);
return http;
}
export const promises = {
resolve: value => Promise.resolve(value),
reject: error => Promise.reject(error),
all: values => Promise.all(values),
defer() {
let resolve, reject;
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
return { promise, resolve, reject };
},
};
+24
View File
@@ -0,0 +1,24 @@
const pending = new Map();
// Generated from the completed library bundles, so deployments invalidate caches.
const assets = __LAZY_ASSETS__;
export function loadLibrary(name) {
if (!pending.has(name)) {
const script = document.createElement("script");
script.src = assets[name];
const promise = new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = () => {
pending.delete(name);
script.remove();
reject(new Error(`Unable to load ${name}. Please retry.`));
};
});
pending.set(name, promise);
document.head.appendChild(script);
}
return pending.get(name);
}
export async function loadEditor() {
await loadLibrary("editor");
ace.config.set("basePath", "/script/external/ace/");
}
+139
View File
@@ -0,0 +1,139 @@
import { loadLibrary } from "./lazy-assets.js";
import { createApp, h, watch, provide, inject, onBeforeUnmount } from "vue";
import { createRouter, createWebHistory, RouterView } from "vue-router";
import { pageRoutes } from "./routes.js";
import { templates } from "./templates.js";
import { initializeTemplate } from "./template-state.js";
import { createPageState, createTimers } from "./state.js";
import { createHttp, promises } from "./http.js";
import { mainController } from "./app.js";
import { createQuotaService } from "./quota.js";
import * as formatters from "./formatters.js";
import translations from "../i18n/locale-en.json";
import { components, codeEditor, paperScrollspy } from "./components.js";
import Tree from "./tree.js";
import { fieldDirective, formDirective, submitForm } from "./forms.js";
const sessionKey = Symbol("session");
export const serverPaths = /^\/(w|api|github)(\/|$)/;
function translate(key, params = {}) {
const message = key?.split(".").reduce((value, part) => value?.[part], translations);
return String(message ?? key ?? "").replace(/{{\s*([^}]+?)\s*}}/g, (_, name) => params[name] ?? "");
}
const fmt = { ...formatters, translate };
export function safeUrl(value) {
if (value == null || value === "") return value;
try {
const url = new URL(value, window.location.href);
return ["http:", "https:", "mailto:", "tel:", "blob:"].includes(url.protocol) ? value : undefined;
} catch { return undefined; }
}
export function mountApplication(target = "#app", options = {}) {
const events = new Map();
const http = createHttp(options.fetch);
let root;
let router;
function services() {
const timers = createTimers();
const location = {
url(value) { if (value === undefined) return router.currentRoute.value.fullPath; router.push(value); return location; },
path() { return router.currentRoute.value.path; },
search() { return router.currentRoute.value.query; },
};
const params = new Proxy({}, { get(_, key) {
const value = router.currentRoute.value.params[key];
return Array.isArray(value) ? value.join("/") : value;
} });
return { http, ...timers, location, params, window, promises, html: { trustAsHtml: value => value }, translate: (key, params) => Promise.resolve(translate(key, params)), quotaService: createQuotaService(http) };
}
function pageComponent(definition) {
return {
name: definition.template,
setup() {
const state = createPageState(inject(sessionKey), events);
definition.setup(state, services());
initializeTemplate(definition.template, state);
const cache = [];
return () => templates[definition.template](state, cache);
},
};
}
router = createRouter({
history: options.history || createWebHistory(),
routes: pageRoutes.map(definition => definition.redirect ? definition : ({
path: definition.path,
component: pageComponent(definition),
meta: { title: definition.title, preserveExplorer: definition.preserveExplorer },
})),
});
const app = createApp({
setup() {
root = createPageState(null, events);
root.fmt = fmt;
root.window = window;
root.Math = Math;
root.sanitize = value => DOMPurify.sanitize(value ?? "");
root.submitForm = submitForm;
root.safeUrl = safeUrl;
const context = services();
mainController(root, context.http, context.location, context.timeout);
provide(sessionKey, root);
watch(() => root.title, title => { document.title = title || "Anonymous GitHub"; });
const headerCache = [];
const navigate = event => {
const anchor = event.target.closest?.("a[href]");
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !anchor || anchor.hasAttribute("download") || (anchor.target && anchor.target !== "_self") || anchor.dataset.toggle) return;
const href = anchor.getAttribute("href");
if (href.startsWith("#")) return;
const url = new URL(anchor.href, window.location.href);
if (url.origin !== window.location.origin || serverPaths.test(url.pathname)) return;
event.preventDefault();
router.push(url.pathname + url.search + url.hash);
$("#navbarSupportedContent.show").collapse("hide");
};
document.addEventListener("click", navigate);
onBeforeUnmount(() => document.removeEventListener("click", navigate));
return () => [
h("header", { class: "app-header" }, templates["partials/header.htm"](root, headerCache)),
h("main", { class: "app-view align-items-stretch w-100" }, h(RouterView, null, {
default: ({ Component, route }) => Component ? h(Component, { key: route.meta.preserveExplorer ? route.matched[0]?.path : route.path }) : null,
})),
h("div", { class: "position-fixed p-3", style: { zIndex: 999999999, right: 0, bottom: 0 } }, root.toasts.map((toast, i) => h("div", { class: "toast show", role: "alert", "aria-live": "assertive", "aria-atomic": "true", key: i }, [
h("div", { class: "toast-header" }, [h("strong", { class: "mr-auto" }, toast.title), h("button", { type: "button", class: "ml-2 mb-1 close", "aria-label": "Close", onClick: () => root.removeToast(toast) }, "×")]),
h("div", { class: "toast-body" }, toast.body),
]))),
];
},
});
for (const [name, component] of Object.entries(components)) app.component(name, component);
app.component("Tree", Tree);
app.component("PartialView", {
props: ["name", "state"],
setup(props) { const cache = []; return () => templates[props.name](props.state, cache); },
});
app.directive("field", fieldDirective);
app.directive("form", formDirective);
app.directive("code-editor", codeEditor);
app.directive("paper-scrollspy", paperScrollspy);
app.use(router);
router.beforeEach(async to => {
root?.emit("routeLeave");
if (/^\/(r|repository|anonymize|pull-request-anonymize|gist-anonymize|pr|gist)(\/|$)/.test(to.path)) {
await loadLibrary("markdown");
}
if (/^\/(r|repository)\//.test(to.path)) await loadLibrary("org");
});
router.afterEach(to => {
if (!root) return;
root.title = to.meta.title;
root.emit("routeChange", { title: to.meta.title });
root.emit("routeUpdate", { title: to.meta.title });
const view = document.querySelector(".app-view");
if (!to.meta.preserveExplorer && view) view.scrollTop = 0;
});
app.mount(target);
return { app, router, state: root };
}
if (document.querySelector("#app")) window.anonymousApp = mountApplication();
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+21 -36
View File
@@ -1,23 +1,12 @@
/** import { h, ref, watch, onMounted, onBeforeUnmount } from "vue";
* AngularJS PDF viewer directive built on pdf.js.
*
* Replaces the old ng-pdfviewer directive, which drove pdf.js 0.8.505 (2013)
* through the long-removed `PDFJS.getDocument()` global API. That build
* emitted malformed CFF/OpenType data for subsetted fonts the browser
* rejected the font ("OTS parsing error: CFF: Failed to parse Top DICT
* Data") and every glyph fell back to the wrong character, so PDFs rendered
* as garbage text rather than failing outright (#771).
*
* Pages rasterise lazily as they scroll into view: a LaTeX/Quarto report can
* run to dozens of pages, and drawing every canvas up front costs hundreds of
* megabytes. Visibility is computed from element rects rather than an
* IntersectionObserver an observer reports nothing while the tab is
* occluded, which would leave the reader looking at empty placeholders.
*/
angular.module("ngPDFViewer", []).directive("pdfviewer", [ export default {
"$window", name: "pdfviewer",
function ($window) { props: ["src"],
setup(props) {
const host = ref(null);
onMounted(() => {
const element = [host.value];
// How far beyond the viewport to rasterise, so scrolling stays ahead of // How far beyond the viewport to rasterise, so scrolling stays ahead of
// the reader. // the reader.
const PRERENDER_MARGIN = 400; const PRERENDER_MARGIN = 400;
@@ -26,12 +15,7 @@ angular.module("ngPDFViewer", []).directive("pdfviewer", [
// to read zoomed than stretched across a wide monitor. // to read zoomed than stretched across a wide monitor.
const FIT_MAX_WIDTH = 1000; const FIT_MAX_WIDTH = 1000;
return {
restrict: "E",
scope: {
src: "@",
},
link: function (scope, element) {
const container = element[0]; const container = element[0];
container.classList.add("pdf-viewer"); container.classList.add("pdf-viewer");
@@ -231,7 +215,7 @@ angular.module("ngPDFViewer", []).directive("pdfviewer", [
// Match the canvas backing store to the device pixel ratio, else text // Match the canvas backing store to the device pixel ratio, else text
// is visibly soft on HiDPI screens. // is visibly soft on HiDPI screens.
function renderPage(page, slot) { function renderPage(page, slot) {
const ratio = $window.devicePixelRatio || 1; const ratio = window.devicePixelRatio || 1;
const unscaled = page.getViewport({ scale: 1 }); const unscaled = page.getViewport({ scale: 1 });
const cssWidth = parseFloat(slot.style.width); const cssWidth = parseFloat(slot.style.width);
if (!cssWidth) return; if (!cssWidth) return;
@@ -300,7 +284,7 @@ angular.module("ngPDFViewer", []).directive("pdfviewer", [
function schedule() { function schedule() {
if (scheduled) return; if (scheduled) return;
scheduled = true; scheduled = true;
$window.requestAnimationFrame(function () { window.requestAnimationFrame(function () {
scheduled = false; scheduled = false;
updateCurrentPage(); updateCurrentPage();
renderVisible(); renderVisible();
@@ -317,8 +301,8 @@ angular.module("ngPDFViewer", []).directive("pdfviewer", [
ran = true; ran = true;
renderVisible(); renderVisible();
} }
$window.requestAnimationFrame(once); window.requestAnimationFrame(once);
$window.setTimeout(once, 50); window.setTimeout(once, 50);
} }
// Reset every page to a placeholder at the current width, then redraw // Reset every page to a placeholder at the current width, then redraw
@@ -402,15 +386,16 @@ angular.module("ngPDFViewer", []).directive("pdfviewer", [
} }
pages.addEventListener("scroll", schedule, { passive: true }); pages.addEventListener("scroll", schedule, { passive: true });
angular.element($window).on("resize", onResize); $(window).on("resize", onResize);
syncToolbar(); syncToolbar();
scope.$watch("src", load); watch(() => props.src, load, { immediate: true });
scope.$on("$destroy", function () { onBeforeUnmount(function () {
pages.removeEventListener("scroll", schedule); pages.removeEventListener("scroll", schedule);
angular.element($window).off("resize", onResize); $(window).off("resize", onResize);
teardown(); teardown();
}); });
});
return () => h("pdfviewer", { ref: host });
}, },
}; };
},
]);
+2
View File
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
export const createQuotaService = function (http) {
function decorate(q) {
q = q || { used: 0, total: 0 };
q.unlimited = !q.total;
q.percent = q.unlimited ? 0 : Math.min(100, (q.used * 100) / q.total);
q.level = q.unlimited
? "unlimited"
: q.percent >= 95
? "danger"
: q.percent >= 80
? "warn"
: "ok";
return q;
}
return {
decorate: decorate,
load: function () {
return http.get("/api/user/quota").then((res) => {
const quota = res.data || {};
quota.storage = decorate(quota.storage);
quota.file = decorate(quota.file);
quota.repository = decorate(quota.repository);
return quota;
});
},
};
};
+32
View File
@@ -0,0 +1,32 @@
import * as pages from "./app.js";
import * as admin from "./admin.js";
export const pageRoutes = [
{path: "/", template: "partials/home.htm", title: "Anonymous GitHub Share the code, not the author", preserveExplorer: false, setup: (state, services) => pages.homeController(state, services.http, services.location, services.window, services.timeout)},
{path: "/dashboard", template: "partials/dashboard.htm", title: "Your anonymizations Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.unifiedDashboardController(state, services.http, services.location, services.promises, services.window, services.quotaService)},
{"path":"/pr-dashboard","redirect":"/dashboard"},
{path: "/anonymize/:repoId?", template: "partials/anonymize.htm", title: "New anonymization Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.anonymizeController(state, services.http, services.html, services.params, services.location, services.translate, services.timeout)},
{path: "/pull-request-anonymize/:pullRequestId?", template: "partials/anonymize.htm", title: "Anonymize a pull request Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.anonymizeController(state, services.http, services.html, services.params, services.location, services.translate, services.timeout)},
{path: "/gist-anonymize/:gistId?", template: "partials/anonymize.htm", title: "Anonymize a gist Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.anonymizeController(state, services.http, services.html, services.params, services.location, services.translate, services.timeout)},
{path: "/status/:repoId", template: "partials/status.htm", title: "Repository status Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.statusController(state, services.http, services.params)},
{path: "/conferences", template: "partials/conferences.htm", title: "Your conferences Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.conferencesController(state, services.http, services.location)},
{path: "/conference/new", template: "partials/newConference.htm", title: "New conference Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.newConferenceController(state, services.http, services.location, services.params)},
{path: "/conference/:conferenceId/edit", template: "partials/newConference.htm", title: "Edit conference Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.newConferenceController(state, services.http, services.location, services.params)},
{path: "/conference/:conferenceId", template: "partials/conference.htm", title: "Conference Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.conferenceController(state, services.http, services.location, services.params)},
{path: "/faq", template: "partials/faq.htm", title: "FAQ Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.faqController(state, services.http)},
{path: "/profile", template: "partials/profile.htm", title: "Your settings Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.profileController(state, services.http, services.translate, services.timeout, services.quotaService)},
{path: "/claim", template: "partials/claim.htm", title: "Claim an anonymization Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.claimController(state, services.http, services.location)},
{path: "/pr/:pullRequestId/:path(.*)*", template: "partials/pullRequest.htm", title: "Anonymous pull request Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.pullRequestController(state, services.http, services.location, services.params, services.html)},
{path: "/gist/:gistId/:path(.*)*", template: "partials/gist.htm", title: "Anonymous gist Anonymous GitHub", preserveExplorer: false, setup: (state, services) => pages.gistController(state, services.http, services.location, services.params, services.html)},
{path: "/r/:repoId/:path(.*)*", template: "partials/explorer.htm", title: "Anonymous repository Anonymous GitHub", preserveExplorer: true, setup: (state, services) => pages.exploreController(state, services.http, services.location, services.params, services.html, services.promises)},
{path: "/repository/:repoId/:path(.*)*", template: "partials/explorer.htm", title: "Anonymous repository Anonymous GitHub", preserveExplorer: true, setup: (state, services) => pages.exploreController(state, services.http, services.location, services.params, services.html, services.promises)},
{path: "/admin/", template: "partials/admin/overview.htm", title: "Admin · Overview Anonymous GitHub", preserveExplorer: false, setup: (state, services) => admin.overviewAdminController(state, services.http, services.location, services.interval)},
{path: "/admin/repositories", template: "partials/admin/repositories.htm", title: "Admin · Repositories Anonymous GitHub", preserveExplorer: false, setup: (state, services) => admin.repositoriesAdminController(state, services.http, services.location)},
{path: "/admin/users", template: "partials/admin/users.htm", title: "Admin · Users Anonymous GitHub", preserveExplorer: false, setup: (state, services) => admin.usersAdminController(state, services.http, services.location)},
{path: "/admin/users/:username", template: "partials/admin/user.htm", title: "Admin · User details Anonymous GitHub", preserveExplorer: false, setup: (state, services) => admin.userAdminController(state, services.http, services.location, services.params)},
{path: "/admin/conferences", template: "partials/admin/conferences.htm", title: "Admin · Conferences Anonymous GitHub", preserveExplorer: false, setup: (state, services) => admin.conferencesAdminController(state, services.http, services.location)},
{path: "/admin/queues", template: "partials/admin/queues.htm", title: "Admin · Queues Anonymous GitHub", preserveExplorer: false, setup: (state, services) => admin.queuesAdminController(state, services.http, services.location, services.interval, services.timeout)},
{path: "/admin/errors", template: "partials/admin/errors.htm", title: "Admin · Errors Anonymous GitHub", preserveExplorer: false, setup: (state, services) => admin.errorsAdminController(state, services.http, services.location, services.interval)},
{path: "/404", template: "partials/404.htm", title: "Page not found Anonymous GitHub", preserveExplorer: false, setup: () => {}},
{path: "/:pathMatch(.*)*", template: "partials/404.htm", title: "Page not found Anonymous GitHub", setup: () => {}},
];
+78
View File
@@ -0,0 +1,78 @@
import { reactive, watch, nextTick, onScopeDispose } from "vue";
export function getPath(object, path) {
return path?.split(".").reduce((value, key) => value?.[key], object);
}
// Page state inherits the shared session without copying it. All mutations are
// tracked by Vue, including those made by fetch callbacks and browser events.
export function createPageState(parent = null, events = new Map()) {
const own = reactive({});
const cleanups = [];
let disposed = false;
const state = new Proxy(own, {
get(target, key) {
// Forward Vue metadata so writes through this proxy notify render effects.
if ((typeof key === "string" && key.startsWith("__v_")) || Reflect.has(target, key)) return Reflect.get(target, key);
return parent?.[key];
},
});
Object.defineProperty(own, "parent", { value: parent, configurable: true });
Object.defineProperty(own, "viewState", { get: () => state });
state.watch = (source, callback, deep = false) => {
const read = typeof source === "function" ? () => source(state) : () => getPath(state, source);
const stop = watch(read, callback, { deep, flush: "post" });
cleanups.push(stop);
nextTick(() => { if (!disposed) callback(read(), read()); });
return stop;
};
state.watchGroup = (sources, callback) => state.watch(() => sources.map(s => getPath(state, s)), callback, true);
state.on = (name, callback) => {
if (name === "dispose") { cleanups.push(callback); return () => {}; }
if (!events.has(name)) events.set(name, new Set());
events.get(name).add(callback);
const off = () => events.get(name)?.delete(callback);
cleanups.push(off);
return off;
};
state.emit = (name, ...args) => {
for (const callback of [...(events.get(name) || [])]) callback({}, ...args);
};
state.dispose = () => {
disposed = true;
cleanups.splice(0).reverse().forEach(fn => fn());
};
onScopeDispose(state.dispose);
return state;
}
export function createTimers() {
const timeouts = new Set();
const intervals = new Set();
let disposed = false;
const timeout = (callback, delay = 0) => {
if (disposed) return;
const id = setTimeout(() => { timeouts.delete(id); callback(); }, delay);
timeouts.add(id);
return id;
};
timeout.cancel = id => { clearTimeout(id); timeouts.delete(id); };
const interval = (callback, delay) => {
if (disposed) return;
const id = setInterval(callback, delay);
intervals.add(id);
return id;
};
interval.cancel = id => { clearInterval(id); intervals.delete(id); };
onScopeDispose(() => { disposed = true; timeouts.forEach(clearTimeout); intervals.forEach(clearInterval); });
return { timeout, interval };
}
export function createListeners() {
const cleanups = [];
onScopeDispose(() => cleanups.forEach(fn => fn()));
return (target, type, callback, options) => {
target.addEventListener(type, callback, options);
cleanups.push(() => target.removeEventListener(type, callback, options));
};
}
+27
View File
@@ -0,0 +1,27 @@
import { computed } from "vue";
// Local state formerly initialized by templates; list aliases are computed.
export function initializeTemplate(name, state) {
if (name === "partials/explorer.htm") {
state.sidebarCollapsed = window.matchMedia?.("(max-width: 767px)").matches || false;
}
if (name === "partials/anonymize.htm") state.prTabState = { active: state.options.diff ? "diff" : "comments" };
if (name === "partials/gist.htm") state.tabState = { active: state.details?.files ? "files" : "comments" };
if (["partials/conferences.htm", "partials/conference.htm"].includes(name)) {
state.statusLabels = { ready: "Ready", expired: "Expired", removed: "Removed" };
}
const lists = {
"partials/dashboard.htm": ["filteredItems", () => state.items, "itemFilter"],
"partials/conferences.htm": ["filteredConferences", () => state.conferences, "conferenceFilter"],
"partials/conference.htm": ["filteredRepositories", () => state.conference?.repositories, "repoFiler"],
"partials/admin/repositories.htm": ["filteredRepositories", () => state.repositories, "repoFiler"],
"partials/admin/user.htm": ["filteredRepositories", () => state.repositories, "repoFiler"],
"partials/admin/users.htm": ["filteredUsers", () => state.users, "userFiler"],
"partials/admin/conferences.htm": ["filteredConferences", () => state.conferences],
};
const list = lists[name];
if (list) {
const value = computed(() => state.fmt.orderBy(state.fmt.filter(list[1](), state[list[2]]), state.orderBy));
Object.defineProperty(state, list[0], { configurable: true, get: () => value.value });
}
}
+54
View File
@@ -0,0 +1,54 @@
import { render as template0 } from "../partials/404.htm";
import { render as template1 } from "../partials/admin/conferences.htm";
import { render as template2 } from "../partials/admin/errors.htm";
import { render as template3 } from "../partials/admin/overview.htm";
import { render as template4 } from "../partials/admin/queues.htm";
import { render as template5 } from "../partials/admin/repositories.htm";
import { render as template6 } from "../partials/admin/user.htm";
import { render as template7 } from "../partials/admin/users.htm";
import { render as template8 } from "../partials/anonymize.htm";
import { render as template9 } from "../partials/anonymizePullRequest.htm";
import { render as template10 } from "../partials/claim.htm";
import { render as template11 } from "../partials/conference.htm";
import { render as template12 } from "../partials/conferences.htm";
import { render as template13 } from "../partials/dashboard.htm";
import { render as template14 } from "../partials/explorer.htm";
import { render as template15 } from "../partials/faq.htm";
import { render as template16 } from "../partials/gist.htm";
import { render as template17 } from "../partials/header.htm";
import { render as template18 } from "../partials/home.htm";
import { render as template19 } from "../partials/loading.htm";
import { render as template20 } from "../partials/newConference.htm";
import { render as template21 } from "../partials/pageView.htm";
import { render as template22 } from "../partials/pr-dashboard.htm";
import { render as template23 } from "../partials/profile.htm";
import { render as template24 } from "../partials/pullRequest.htm";
import { render as template25 } from "../partials/status.htm";
export const templates = {
"partials/404.htm": template0,
"partials/admin/conferences.htm": template1,
"partials/admin/errors.htm": template2,
"partials/admin/overview.htm": template3,
"partials/admin/queues.htm": template4,
"partials/admin/repositories.htm": template5,
"partials/admin/user.htm": template6,
"partials/admin/users.htm": template7,
"partials/anonymize.htm": template8,
"partials/anonymizePullRequest.htm": template9,
"partials/claim.htm": template10,
"partials/conference.htm": template11,
"partials/conferences.htm": template12,
"partials/dashboard.htm": template13,
"partials/explorer.htm": template14,
"partials/faq.htm": template15,
"partials/gist.htm": template16,
"partials/header.htm": template17,
"partials/home.htm": template18,
"partials/loading.htm": template19,
"partials/newConference.htm": template20,
"partials/pageView.htm": template21,
"partials/pr-dashboard.htm": template22,
"partials/profile.htm": template23,
"partials/pullRequest.htm": template24,
"partials/status.htm": template25
};
+113
View File
@@ -0,0 +1,113 @@
import { h, reactive, ref, computed, watch, nextTick } from "vue";
import { useRoute } from "vue-router";
export function fileHierarchy(files) {
const root = { children: [] };
const directories = new Map([["", root]]);
function directory(path) {
if (directories.has(path)) return directories.get(path);
const index = path.lastIndexOf("/");
const parent = directory(index < 0 ? "" : path.slice(0, index));
const node = { name: path.slice(index + 1), children: [] };
directories.set(path, node);
parent.children.push(node);
return node;
}
for (const file of files || []) {
const path = file.path ? `${file.path}/${file.name}` : file.name;
if (file.size == null) directory(path);
else directory(file.path || "").children.push({ ...file });
}
return root.children;
}
export default {
name: "FileTree",
props: ["file", "parent", "searchQuery", "searchResults", "page"],
setup(props) {
const route = useRoute();
const host = ref(null);
const opens = reactive(Object.create(null));
let focusedPath = null;
const hierarchy = computed(() => fileHierarchy(props.file));
const selectedPath = () => "/" + (Array.isArray(route.params.path) ? route.params.path.join("/") : route.params.path || "");
watch(() => [route.params.repoId, route.params.path], () => {
let path = "";
selectedPath().split("/").filter(Boolean).forEach(part => { path += "/" + part; opens[path] = true; });
}, { immediate: true });
const matching = computed(() => {
if (!props.searchQuery || !props.searchResults) return null;
const files = new Set(), folders = new Set();
props.searchResults.forEach(file => {
files.add(file.path ? `${file.path}/${file.name}` : file.name);
let path = "";
(file.path || "").split("/").filter(Boolean).forEach(part => { path = path ? path + "/" + part : part; folders.add(path); });
});
return { files, folders };
});
const visibleLinks = () => [...host.value.querySelectorAll("li > a")];
const focus = link => {
host.value.querySelector(".tree-focused")?.classList.remove("tree-focused");
if (!link) return;
focusedPath = link.dataset.path;
link.classList.add("tree-focused");
link.scrollIntoView?.({ block: "nearest" });
host.value.focus();
};
function keyboard(event) {
const links = visibleLinks();
const current = host.value.querySelector(".tree-focused") || links[0];
const index = links.indexOf(current);
if (!["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Enter"].includes(event.key)) return;
event.preventDefault();
if (event.key === "ArrowDown") focus(links[(index + 1) % links.length]);
if (event.key === "ArrowUp") focus(links[(index - 1 + links.length) % links.length]);
if (!current) return;
const li = current.closest("li");
if (event.key === "ArrowRight" && li.classList.contains("folder")) {
if (!li.classList.contains("open")) current.click();
else focus(li.querySelector(":scope > ul > li > a"));
}
if (event.key === "ArrowLeft") {
if (li.classList.contains("folder") && li.classList.contains("open")) current.click();
else focus(li.parentElement.closest("li.folder")?.querySelector(":scope > a"));
}
if (event.key === "Enter") current.click();
}
function renderNodes(nodes, parent = "") {
const sorted = [...nodes].sort((a, b) => Number(!!b.children) - Number(!!a.children) || a.name.localeCompare(b.name));
return h("ul", sorted.flatMap(original => {
let node = original, name = node.name;
const originalPath = (parent + "/" + name).slice(1);
if (matching.value && !matching.value.files.has(originalPath) && !matching.value.folders.has(originalPath)) return [];
while (node.children?.length === 1) { node = node.children[0]; name += "/" + node.name; }
const path = parent + "/" + name;
const folder = !!node.children;
const open = matching.value ? opens[path] !== false : !!opens[path];
const truncated = folder && props.page?.options?.truncatedFolders?.includes(path.slice(1));
const count = props.page?.fileCounts?.[path.slice(1)] || 0;
const icon = h("span", { class: folder ? "tree-icon-folder" : "tree-icon-file" });
const label = h("span", { class: "tree-name" }, name);
const onClick = event => {
focus(event.currentTarget);
if (!folder) return;
event.preventDefault();
opens[path] = !open;
if (opens[path] && !node.children.length) props.page.getFiles(path.slice(1));
nextTick(() => focus(visibleLinks().find(link => link.dataset.path === focusedPath)));
};
const link = h("a", {
"data-path": path,
href: folder ? undefined : `/r/${encodeURIComponent(route.params.repoId)}${encodePathForUrl(path)}`,
class: { "tree-focused": focusedPath === path },
onClick,
}, [folder ? h("span", { class: "tree-toggle" }) : parent ? h("span", { class: "tree-spacer" }) : null, icon, label,
truncated ? h("span", { class: "truncated-warning", title: props.page.fmt.translate("WARNINGS.folder_truncated") }, h("i", { class: "fas fa-exclamation-triangle" })) : null,
folder && count ? h("span", { class: "tree-count" }, count) : null]);
return h("li", { key: path, class: { file: true, folder, open, active: selectedPath() === path, truncated }, title: folder ? "" : `Size: ${humanFileSize(node.size || 0)}` }, [link, folder && open ? renderNodes(node.children, path) : null]);
}));
}
return () => h("tree", { ref: host, tabindex: 0, onKeydown: keyboard },
!props.file?.length ? "Empty repository" : matching.value?.files.size === 0 ? h("div", { class: "tree-search-empty" }, "No files found") : renderNodes(hierarchy.value));
},
};
+71 -2
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -2,9 +2,10 @@
* Dev proxy for local UI iteration. * Dev proxy for local UI iteration.
* *
* Serves the local `public/` folder for HTML/CSS/JS/partials/images so you * Serves the local `public/` folder for HTML/CSS/JS/partials/images so you
* see your design changes instantly, and proxies everything else (API, * see your built UI changes, and proxies everything else (API,
* auth, repo content, ) to the live https://anonymous.4open.science site. * auth, repo content, ) to the live https://anonymous.4open.science site.
* *
* npm run build:ui # rebuild after changing UI scripts or templates
* npm run dev:ui # default port 4001 * npm run dev:ui # default port 4001
* PORT=5000 npm run dev:ui * PORT=5000 npm run dev:ui
* *
+9 -3
View File
@@ -20,6 +20,8 @@ describe("asset build", function () {
beforeEach(function () { beforeEach(function () {
directory = fs.mkdtempSync(path.join(os.tmpdir(), "anonymous-assets-")); directory = fs.mkdtempSync(path.join(os.tmpdir(), "anonymous-assets-"));
fs.mkdirSync(path.join(directory, "public/script"), { recursive: true });
fs.writeFileSync(path.join(directory, "public/script/main.js"), 'globalThis.appLoaded = true;');
for (const file of Object.values(groups).flat()) { for (const file of Object.values(groups).flat()) {
const target = path.join(directory, file); const target = path.join(directory, file);
fs.mkdirSync(path.dirname(target), { recursive: true }); fs.mkdirSync(path.dirname(target), { recursive: true });
@@ -45,14 +47,18 @@ describe("asset build", function () {
it("preserves script dependencies and CSS precedence and hashes completed assets", function () { it("preserves script dependencies and CSS precedence and hashes completed assets", function () {
const result = build(); const result = build();
expect(result.status, result.stderr).to.equal(0); expect(result.status, result.stderr).to.equal(0);
for (const [bundle, group] of [["core", "coreJsFiles"], ["vendor", "vendorJsFiles"], ["mermaid", "mermaidFiles"]]) { for (const [bundle, group] of [["core", "coreJsFiles"], ["markdown", "markdownFiles"], ["pdf", "pdfFiles"], ["editor", "editorFiles"], ["notebook", "notebookFiles"], ["org", "orgFiles"], ["mermaid", "mermaidFiles"]]) {
const context = { assetOrder: [] }; const context = { assetOrder: [] };
vm.runInNewContext(fs.readFileSync(path.join(directory, `public/script/${bundle}.min.js`), "utf8"), context); vm.runInNewContext(fs.readFileSync(path.join(directory, `public/script/${bundle}.min.js`), "utf8"), context);
expect(context.assetOrder).to.deep.equal(groups[group]); expect(context.assetOrder).to.deep.equal(groups[group]);
} }
expect(fs.readFileSync(path.join(directory, "public/css/all.min.css"), "utf8")).to.equal(".cascade{color:#00f}".repeat(groups.cssFiles.length - 1) + ".cascade{color:red}"); expect(fs.readFileSync(path.join(directory, "public/css/all.min.css"), "utf8")).to.equal(".cascade{color:#00f}".repeat(groups.cssFiles.length - 1) + ".cascade{color:red}");
const manifest = JSON.parse(fs.readFileSync(path.join(directory, "public/asset-manifest.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(directory, "public/asset-manifest.json"), "utf8"));
expect(Object.keys(manifest)).to.have.length(4); expect(Object.keys(manifest)).to.have.length(9);
const appContext = {};
vm.runInNewContext(fs.readFileSync(path.join(directory, "public/script/vendor.min.js"), "utf8"), appContext);
expect(appContext.appLoaded).to.equal(true);
for (const [name, hashed] of Object.entries(manifest)) { for (const [name, hashed] of Object.entries(manifest)) {
const content = fs.readFileSync(path.join(directory, "public", name.endsWith(".css") ? "css" : "script", name)); const content = fs.readFileSync(path.join(directory, "public", name.endsWith(".css") ? "css" : "script", name));
const hash = require("node:crypto").createHash("md5").update(content).digest("hex").slice(0, 10); const hash = require("node:crypto").createHash("md5").update(content).digest("hex").slice(0, 10);
@@ -69,7 +75,7 @@ describe("asset build", function () {
}); });
it("fails on invalid JavaScript without publishing a manifest", function () { it("fails on invalid JavaScript without publishing a manifest", function () {
fs.writeFileSync(path.join(directory, groups.vendorJsFiles[0]), "function {"); fs.writeFileSync(path.join(directory, groups.pdfFiles[0]), "function {");
const result = build(); const result = build();
expect(result.status).not.to.equal(0); expect(result.status).not.to.equal(0);
expect(result.stderr).to.include("uglify"); expect(result.stderr).to.include("uglify");
+1 -1
View File
@@ -44,6 +44,6 @@ describe("dashboard .cell-conf overflow fix", function () {
it("exposes the full conference value via a title attribute for truncated text", function () { it("exposes the full conference value via a title attribute for truncated text", function () {
const html = fs.readFileSync(htmlPath, "utf8"); const html = fs.readFileSync(htmlPath, "utf8");
expect(html).to.match(/cell-conf[\s\S]*?title="\{\{item\.conference\}\}"/); expect(html).to.match(/cell-conf[\s\S]*?:title="\(item\?\.conference\)"/);
}); });
}); });
+9 -41
View File
@@ -12,48 +12,16 @@ const vm = require("vm");
*/ */
const root = path.join(__dirname, ".."); const root = path.join(__dirname, "..");
const appJs = fs.readFileSync(path.join(root, "public", "script", "app.js"), "utf8");
const css = fs.readFileSync(path.join(root, "public", "css", "style.css"), "utf8"); const css = fs.readFileSync(path.join(root, "public", "css", "style.css"), "utf8");
const html = fs.readFileSync(path.join(root, "public", "partials", "dashboard.htm"), "utf8"); const html = fs.readFileSync(path.join(root, "public", "partials", "dashboard.htm"), "utf8");
// Load app.js with a stub `angular` that records filter factories, so the // Exercise the same formatting functions used by the Vue templates.
// filters can be exercised without a browser.
function loadFilters() { function loadFilters() {
const filters = {}; const source = fs.readFileSync(path.join(__dirname, "../public/script/formatters.js"), "utf8");
const chain = new Proxy( const names = [...source.matchAll(/export const (\w+)/g)].map(match => match[1]);
{}, const sandbox = { window: {}, console };
{ vm.runInNewContext(source.replace(/export const/g, "var").replace(/export function/g, "function") + "\nthis.formatters = {" + names.join(",") + "};", sandbox);
get(_, prop) { return Object.fromEntries(Object.entries(sandbox.formatters).map(([name, fn]) => [name, () => fn]));
if (prop === "filter") {
return (name, factory) => {
filters[name] = Array.isArray(factory) ? factory[factory.length - 1] : factory;
return chain;
};
}
return () => chain;
},
}
);
// jQuery is used at the top level of app.js for a couple of global
// listeners; a self-returning proxy absorbs those calls.
const jq = new Proxy(function () {}, {
get: () => jq,
apply: () => jq,
});
const sandbox = {
angular: { module: () => chain, element: () => ({}) },
$: jq,
jQuery: jq,
window: {},
document: { addEventListener() {}, querySelector: () => null },
localStorage: { getItem: () => null, setItem() {} },
navigator: { language: "en-US" },
console,
};
sandbox.window = sandbox;
vm.createContext(sandbox);
vm.runInContext(appJs, sandbox, { filename: "app.js" });
return filters;
} }
function contrast(hexA, hexB) { function contrast(hexA, hexB) {
@@ -125,14 +93,14 @@ describe("dashboard UI", function () {
expect(divider).to.be.greaterThan(-1); expect(divider).to.be.greaterThan(-1);
}); });
it("labels hidden statuses as hidden, not as active filters", function () { it("labels hidden statuses as hidden, not as active filters", function () {
expect(html).to.match(/Hiding \{\{statusKeyLabels\[f\]\}\}/); expect(html).to.match(/Hiding \{\{\s*statusKeyLabels\[f\]\s*\}\}/);
}); });
it("shows an unlimited quota without a full bar", function () { it("shows an unlimited quota without a full bar", function () {
expect(html).to.match(/quota-fill" ng-if="!quota\[q\.key\]\.unlimited"/); expect(html).to.match(/quota-fill" v-if="!quota\[q\?\.key\]\.unlimited"/);
expect(html).to.not.match(/bg-success|bg-warning|bg-danger/); expect(html).to.not.match(/bg-success|bg-warning|bg-danger/);
}); });
it("right-aligns the Views column and marks it sortable", function () { it("right-aligns the Views column and marks it sortable", function () {
expect(html).to.match(/class="num"[^>]*>\s*<button type="button" class="sortable"[^>]*ng-click="setSort\('pageView'\)"/); expect(html).to.match(/class="num"[^>]*>\s*<button type="button" class="sortable"[^>]*@click="setSort\(&#x27;pageView&#x27;\)"/);
}); });
}); });
+27 -49
View File
@@ -8,15 +8,7 @@ const source = fs.readFileSync(path.join(__dirname, "../public/script/app.js"),
function harness(date) { function harness(date) {
const defs = {}, routes = {}, timers = new Map(); const defs = {}, routes = {}, timers = new Map();
let timerId = 0; let timerId = 0;
const chain = new Proxy({}, { get: (_, method) => (...args) => { const context = { reactive: value => value, console, Map, Set, Date: date || Date,
if (["controller", "directive"].includes(method)) defs[args[0]] = args[1];
if (method === "config") {
const route = { when(p, opt) { routes[p] = opt; return route; }, otherwise() {} };
args[0].at(-1)(route, { html5Mode() {} }, { useStaticFilesLoader() {}, preferredLanguage() {} });
}
return chain;
} });
const context = { angular: { module: () => chain }, console, Map, Set, Date: date || Date,
navigator: { platform: "Linux" }, document: { location: { pathname: "/r/repo" }, addEventListener() {}, querySelector() {} }, navigator: { platform: "Linux" }, document: { location: { pathname: "/r/repo" }, addEventListener() {}, querySelector() {} },
window: {}, Prism: { highlightAll() {} }, encodeURIComponent, window: {}, Prism: { highlightAll() {} }, encodeURIComponent,
encodePathForUrl: p => p.split("/").map(encodeURIComponent).join("/"), encodePathForUrl: p => p.split("/").map(encodeURIComponent).join("/"),
@@ -25,9 +17,17 @@ function harness(date) {
setTimeout: fn => { timers.set(++timerId, fn); return timerId; }, setTimeout: fn => { timers.set(++timerId, fn); return timerId; },
clearTimeout: id => timers.delete(id), setInterval: () => 0, clearInterval() {}, clearTimeout: id => timers.delete(id), setInterval: () => 0, clearInterval() {},
}; };
vm.runInNewContext(source, context); context.createTimers = () => ({ timeout: Object.assign(context.setTimeout, { cancel: context.clearTimeout }), interval: Object.assign(context.setInterval, { cancel: context.clearInterval }) });
context.createListeners = () => (target, name, callback) => target.addEventListener(name, callback);
const names = [...source.matchAll(/export const (\w+)/g)].map(match => match[1]);
vm.runInNewContext(source.replace(/^import .*;$/gm, "").replace(/export const/g, "var") + "\nthis.pageSetups = {" + names.join(",") + "};", context);
Object.assign(defs, context.pageSetups);
const routeSource = fs.readFileSync(path.join(__dirname, "../public/script/routes.js"), "utf8");
const routeContext = { pages: defs, admin: {} };
vm.runInNewContext(routeSource.replace(/^import .*;$/gm, "").replace("export const pageRoutes", "this.pageRoutes"), routeContext);
routeContext.pageRoutes.forEach(route => { routes[route.path] = route; });
const events = {}, watches = {}; const events = {}, watches = {};
const scope = { $new() { return { $destroy() {} }; }, $on: (key, fn) => { (events[key] ||= []).push(fn); }, $watch: (key, fn) => { watches[key] = fn; }, $apply() {}, $applyAsync() {} }; const scope = { $new() { return { dispose() {} }; }, on: (key, fn) => { (events[key] ||= []).push(fn); }, watch: (key, fn) => { watches[key] = fn; }, $apply() {}, $applyAsync() {} };
const requests = []; const requests = [];
const http = {}; const http = {};
for (const method of ["get", "post"]) http[method] = (url, body) => new Promise((resolve, reject) => requests.push({ method, url, body, resolve, reject })); for (const method of ["get", "post"]) http[method] = (url, body) => new Promise((resolve, reject) => requests.push({ method, url, body, resolve, reject }));
@@ -39,33 +39,12 @@ function harness(date) {
} }
function explorer() { function explorer() {
const h = harness(); h.params = { repoId: "repo", path: "README.md" }; const h = harness(); h.params = { repoId: "repo", path: "README.md" };
h.defs.exploreController.at(-1)(h.scope, h.http, { url: () => "/r/repo/README.md" }, h.params, { trustAsHtml: x => x }, h.q); h.defs.exploreController(h.scope, h.http, { url: () => "/r/repo/README.md" }, h.params, { trustAsHtml: x => x }, h.q);
h.navigate = path => { h.params.path = path; h.emit("$routeUpdate"); }; h.navigate = path => { h.params.path = path; h.emit("routeUpdate"); };
return h; return h;
} }
describe("frontend production regressions", function () { describe("frontend production regressions", function () {
it("keeps filenames and folder paths out of compiled Angular templates", function () {
const h = harness(); let template;
const element = { html() {}, append() {}, 0: { addEventListener() {}, setAttribute() {} } };
h.scope.file = [{ name: '{{constructor.constructor("window.probe=1")()}}.txt', path: "", size: 1 }];
h.scope.$parent = {};
h.defs.tree[0]().controller.at(-1)(element, h.scope, {}, html => { template = html; return () => {}; });
h.watches.file(h.scope.file);
expect(template).not.to.include("constructor.constructor");
expect(template).to.include('ng-bind="treeNodes[0].name"');
expect(h.scope.treeNodes[0].name).to.equal(h.scope.file[0].name);
});
it("renders directories whose names collide with Object.prototype", function () {
const h = harness(); let template;
const element = { html() {}, 0: { addEventListener() {}, setAttribute() {} } };
h.scope.file = [{ name: "constructor", path: "" }, { name: "index.js", path: "constructor", size: 1 }];
h.scope.$parent = {};
h.defs.tree[0]().controller.at(-1)(element, h.scope, {}, html => { template = html; return () => {}; });
expect(() => h.watches.file(h.scope.file)).not.to.throw();
expect(template).to.include("treeNodes");
expect(h.scope.treeNodes[0].path).to.equal("/constructor/index.js");
});
it("sanitizes Org output before trusting it", async function () { it("sanitizes Org output before trusting it", async function () {
const h = explorer(); let untrusted; const h = explorer(); let untrusted;
h.context.Org = { Parser: function () { this.parse = () => ({ convert: () => ({ toString: () => '<img onerror="probe()">' }) }); }, ConverterHTML: {} }; h.context.Org = { Parser: function () { this.parse = () => ({ convert: () => ({ toString: () => '<img onerror="probe()">' }) }); }, ConverterHTML: {} };
@@ -87,21 +66,20 @@ describe("frontend production regressions", function () {
}); });
it("reloads the repository when only its ID changes", function () { it("reloads the repository when only its ID changes", function () {
const h = explorer(); h.scope.files = [{ name: "old", path: "old" }]; const h = explorer(); h.scope.files = [{ name: "old", path: "old" }];
h.params.repoId = "new-repo"; h.emit("$routeUpdate"); h.params.repoId = "new-repo"; h.emit("routeUpdate");
expect(h.scope.repoId).to.equal("new-repo"); expect(h.scope.files).to.have.length(0); expect(h.scope.repoId).to.equal("new-repo"); expect(h.scope.files).to.have.length(0);
expect(h.requests.at(-1).url).to.equal("/api/repo/new-repo/options"); expect(h.requests.at(-1).url).to.equal("/api/repo/new-repo/options");
}); });
it("reloads PR and Gist controllers for new resource IDs", function () { it("reloads PR and Gist controllers for new resource IDs", function () {
const h = harness(); const h = harness();
for (const route of ["/pr/:pullRequestId/:path*?", "/gist/:gistId/:path*?"]) { for (const route of ["/pr/:pullRequestId/:path(.*)*", "/gist/:gistId/:path(.*)*"]) {
expect(h.routes[route].reloadOnUrl).not.to.equal(false); expect(h.routes[route].preserveExplorer).to.equal(false);
expect(h.routes[route].reloadOnSearch).to.equal(false);
} }
}); });
it("includes PR comment authors and bodies in the preview batch", async function () { it("includes PR comment authors and bodies in the preview batch", async function () {
const h = harness(); const pending = new Map(); let id = 0; const h = harness(); const pending = new Map(); let id = 0;
const timeout = fn => { pending.set(++id, fn); return id; }; timeout.cancel = id => pending.delete(id); const timeout = fn => { pending.set(++id, fn); return id; }; timeout.cancel = id => pending.delete(id);
h.defs.anonymizeController.at(-1)(h.scope, h.http, {}, {}, {}, () => {}, timeout); h.defs.anonymizeController(h.scope, h.http, {}, {}, {}, () => {}, timeout);
h.scope.detectedType = "pr"; h.scope.terms = "Alice"; h.scope.detectedType = "pr"; h.scope.terms = "Alice";
h.scope.details = { pullRequest: { title: "title", comments: [{ author: "Alice", body: "Alice comment" }] } }; h.scope.details = { pullRequest: { title: "title", comments: [{ author: "Alice", body: "Alice comment" }] } };
h.watches.terms(); [...pending.values()][0](); h.watches.terms(); [...pending.values()][0]();
@@ -111,7 +89,7 @@ describe("frontend production regressions", function () {
expect(h.scope.anonymizePrContent("Alice")).to.equal("MASK"); expect(h.scope.anonymizePrContent("Alice")).to.equal("MASK");
}); });
it("selects the diff tab after asynchronous PR loading", async function () { it("selects the diff tab after asynchronous PR loading", async function () {
const h = harness(); h.defs.pullRequestController.at(-1)(h.scope, h.http, {}, { pullRequestId: "pr" }, {}); const h = harness(); h.defs.pullRequestController(h.scope, h.http, {}, { pullRequestId: "pr" }, {});
h.requests[0].resolve({ data: {} }); await h.flush(); h.requests[0].resolve({ data: {} }); await h.flush();
h.requests[1].resolve({ data: { diff: "patch" } }); await h.flush(); h.requests[1].resolve({ data: { diff: "patch" } }); await h.flush();
expect(h.scope.tabState.active).to.equal("diff"); expect(h.scope.tabState.active).to.equal("diff");
@@ -126,17 +104,17 @@ describe("frontend production regressions", function () {
expect(h.scope.fileSearchResults).to.have.length(0); expect(h.scope.fileSearchResults).to.have.length(0);
}); });
it("cancels status polling on destroy, including late responses", async function () { it("cancels status polling on destroy, including late responses", async function () {
const h = harness(); h.defs.statusController.at(-1)(h.scope, h.http, { repoId: "repo" }); const h = harness(); h.defs.statusController(h.scope, h.http, { repoId: "repo" });
h.requests[0].resolve({ data: { status: "preparing" } }); await h.flush(); h.requests[0].resolve({ data: { status: "preparing" } }); await h.flush();
expect(h.timers.size).to.equal(1); const callback = [...h.timers.values()][0]; expect(h.timers.size).to.equal(1); const callback = [...h.timers.values()][0];
h.emit("$destroy"); expect(h.timers.size).to.equal(0); callback(); expect(h.requests).to.have.length(1); h.emit("dispose"); expect(h.timers.size).to.equal(0); callback(); expect(h.requests).to.have.length(1);
const late = harness(); late.defs.statusController.at(-1)(late.scope, late.http, { repoId: "repo" }); const late = harness(); late.defs.statusController(late.scope, late.http, { repoId: "repo" });
late.emit("$destroy"); late.requests[0].resolve({ data: { status: "preparing" } }); await late.flush(); expect(late.timers.size).to.equal(0); late.emit("dispose"); late.requests[0].resolve({ data: { status: "preparing" } }); await late.flush(); expect(late.timers.size).to.equal(0);
}); });
it("translates profile save failures", async function () { it("translates profile save failures", async function () {
const h = harness(); expect(h.defs.profileController).to.include("$translate"); const h = harness(); expect(h.defs.profileController.toString()).to.include("translate");
const timeout = Object.assign(() => 0, { cancel() {} }); const timeout = Object.assign(() => 0, { cancel() {} });
h.defs.profileController.at(-1)(h.scope, h.http, key => Promise.resolve(key), timeout, { load: () => Promise.resolve({}) }); h.defs.profileController(h.scope, h.http, key => Promise.resolve(key), timeout, { load: () => Promise.resolve({}) });
h.scope.saveDefault(); h.requests.at(-1).reject({ data: { error: "not_connected" } }); await h.flush(); h.scope.saveDefault(); h.requests.at(-1).reject({ data: { error: "not_connected" } }); await h.flush();
expect(h.scope.error).to.equal("ERRORS.not_connected"); expect(h.scope.error).to.equal("ERRORS.not_connected");
}); });
@@ -147,7 +125,7 @@ describe("frontend production regressions", function () {
const focused = []; h.focused = focused; const focused = []; h.focused = focused;
const win = { document: { getElementById: id => ({ focus: () => focused.push(id) }) } }; const win = { document: { getElementById: id => ({ focus: () => focused.push(id) }) } };
const timeout = fn => fn(); const timeout = fn => fn();
h.defs.homeController.at(-1)(h.scope, h.http, { url() {} }, win, timeout); h.defs.homeController(h.scope, h.http, { url() {} }, win, timeout);
return h; return h;
} }
it("selects the first feature and switches on click", function () { it("selects the first feature and switches on click", function () {
@@ -179,7 +157,7 @@ describe("frontend production regressions", function () {
it("marks the tabs up as tabs and keeps links out of the buttons", function () { it("marks the tabs up as tabs and keeps links out of the buttons", function () {
expect(home).to.match(/<button[^>]*class="paper-feature-tab"[^>]*role="tab"/); expect(home).to.match(/<button[^>]*class="paper-feature-tab"[^>]*role="tab"/);
expect(home).to.match(/role="tablist"/); expect(home).to.match(/role="tablist"/);
expect(home).to.match(/role="tabpanel"[^>]*aria-labelledby="feature-tab-\{\{f\.key\}\}"/); expect(home).to.match(/role="tabpanel"[^>]*:aria-labelledby=/);
const button = home.slice(home.indexOf('class="paper-feature-tab"'), home.indexOf("</button>")); const button = home.slice(home.indexOf('class="paper-feature-tab"'), home.indexOf("</button>"));
expect(button).to.not.include("<a "); expect(button).to.not.include("<a ");
expect(home).to.not.match(/href="#"/); expect(home).to.not.match(/href="#"/);
@@ -188,7 +166,7 @@ describe("frontend production regressions", function () {
it("keeps the conference end date after the start across December", function () { it("keeps the conference end date after the start across December", function () {
class December extends Date { constructor(...args) { super(...(args.length ? args : ["2026-12-15T12:00:00Z"])); } } class December extends Date { constructor(...args) { super(...(args.length ? args : ["2026-12-15T12:00:00Z"])); } }
const h = harness(December); h.scope.user = {}; const h = harness(December); h.scope.user = {};
h.defs.newConferenceController.at(-1)(h.scope, h.http, {}, {}); h.defs.newConferenceController(h.scope, h.http, {}, {});
expect(h.scope.options.startDate.getFullYear()).to.equal(2027); expect(h.scope.options.startDate.getFullYear()).to.equal(2027);
expect(h.scope.options.endDate.getTime()).to.be.greaterThan(h.scope.options.startDate.getTime()); expect(h.scope.options.endDate.getTime()).to.be.greaterThan(h.scope.options.startDate.getTime());
}); });
+288
View File
@@ -0,0 +1,288 @@
const { expect } = require("chai");
const { JSDOM, VirtualConsole, ResourceLoader } = require("jsdom");
const fs = require("fs");
const path = require("path");
const { URL } = require("node:url");
const { setTimeout: delay } = require("node:timers/promises");
const publicDir = path.join(__dirname, "../public");
const bundles = ["core.min.js", "vendor.min.js"].map(name => fs.readFileSync(path.join(publicDir, "script", name), "utf8"));
async function browser(route = "/", overrides = {}) {
const errors = [], requests = [], assets = [];
const virtualConsole = new VirtualConsole();
virtualConsole.on("jsdomError", error => {
if (!error.message.includes("navigation (except hash changes)")) errors.push(error.message);
});
virtualConsole.on("error", error => errors.push(error?.message || String(error)));
const dom = new JSDOM('<!doctype html><html><head></head><body><div id="app"></div></body></html>', {
resources: new class extends ResourceLoader {
fetch(url) {
assets.push(new URL(url).pathname);
if (/\/pdf\.[a-f0-9]+\.min\.js$/.test(new URL(url).pathname) && dom.window.pdfjsLib) return Promise.resolve(Buffer.from(""));
const pathname = new URL(url).pathname.replace(/\.[a-f0-9]{10}\.min\.js$/, ".min.js");
if (pathname.startsWith("/script/")) return Promise.resolve(fs.readFileSync(path.join(publicDir, pathname)));
return null;
}
}(),
url: "http://localhost" + route, runScripts: "dangerously", pretendToBeVisual: true, virtualConsole,
});
const window = dom.window;
window.matchMedia = () => ({ matches: false });
window.HTMLCanvasElement.prototype.getContext = () => null;
window.fetch = async (url, options = {}) => {
const target = new URL(url);
const request = { url: target, ...options, payload: options.body ? JSON.parse(options.body) : undefined };
requests.push(request);
let data;
const custom = overrides[target.pathname];
if (custom !== undefined) data = typeof custom === "function" ? await custom(request) : custom;
else if (target.pathname === "/api/user") data = null;
else if (target.pathname === "/api/user/default") data = { options: {}, terms: [] };
else if (/\/files\/$/.test(target.pathname)) data = [{ name: "hello.js", path: "", sha: "1", size: 8 }];
else if (/\/files\/counts$/.test(target.pathname)) data = { "": 1 };
else if (target.pathname === "/api/admin/errors") data = { entries: [] };
else if (target.pathname === "/api/conferences/plans") data = [];
else if (/\/options$|\/quota$|\/stats$|\/content$|\/api\/conferences\/[^/]+$/.test(target.pathname)) data = {};
else if (/\/file\//.test(target.pathname)) data = "hello";
else data = [];
const status = data?.__status || 200;
if (data?.__status) data = data.body;
return { ok: status < 400, status, headers: { get: () => typeof data === "string" ? "text/plain" : "application/json" }, text: async () => typeof data === "string" ? data : JSON.stringify(data) };
};
bundles.forEach(bundle => window.eval(bundle));
const app = window.anonymousApp;
await app.router.isReady();
await delay(30);
return {
window, app, errors, requests, assets,
async go(path) { await app.router.push(path); await delay(30); },
async input(selector, value, event = "input") {
const input = window.document.querySelector(selector);
expect(input, selector).not.to.equal(null);
input.value = value;
input.dispatchEvent(new window.Event(event, { bubbles: true }));
await delay(10);
return input;
},
close() { app.app.unmount(); window.close(); },
};
}
describe("Vue 3 UI", function () {
this.timeout(15000);
let ui;
afterEach(() => ui?.close());
it("renders every public and administrative route", async function () {
ui = await browser();
for (const route of ["/faq", "/anonymize", "/gist-anonymize", "/pull-request-anonymize", "/status/test", "/404", "/r/test/", "/repository/test/", "/pr/test/", "/gist/test/"]) {
await ui.go(route);
expect(ui.window.document.querySelector(".app-view").textContent, route + JSON.stringify(ui.errors)).not.to.equal("");
}
ui.app.state.user = { username: "tester", status: "ready", isAdmin: true };
for (const route of ["/dashboard", "/claim", "/profile", "/conferences", "/conference/new", "/conference/test", "/conference/test/edit", "/admin/", "/admin/users", "/admin/users/test", "/admin/repositories", "/admin/conferences", "/admin/queues", "/admin/errors"]) {
await ui.go(route);
expect(ui.window.document.querySelector(".app-view").textContent, route + JSON.stringify(ui.errors)).not.to.equal("");
}
expect(ui.errors).to.deep.equal([]);
});
it("loads document libraries on demand and reuses them across navigation", async function () {
ui = await browser("/dashboard");
expect(ui.assets.filter(url => url.endsWith(".js"))).to.deep.equal([]);
await ui.go("/r/test/README.md");
expect(ui.assets.some(url => /\/markdown\./.test(url))).to.equal(true);
expect(ui.assets.some(url => /\/(pdf|editor|notebook)\./.test(url))).to.equal(false);
await ui.go("/faq");
await ui.go("/r/test/README.md");
expect(ui.assets.filter(url => /\/markdown\./.test(url))).to.have.length(1);
await ui.go("/r/test/hello.js");
expect(ui.assets.some(url => /\/editor\./.test(url))).to.equal(true);
expect(ui.window.document.querySelector(".ace_editor")).not.to.equal(null);
expect(ui.errors).to.deep.equal([]);
});
it("validates a claim, binds input values and renders server validation failures", async function () {
ui = await browser("/claim", { "/api/repo/claim": { __status: 404, body: {} } });
const form = ui.window.document.querySelector("form");
form.dispatchEvent(new ui.window.Event("submit", { bubbles: true, cancelable: true }));
expect(ui.requests.filter(r => r.method === "POST")).to.have.length(0);
await ui.input("#repoUrl", "https://github.com/example/repository");
await ui.input("#repoId", "anonymous-id");
form.dispatchEvent(new ui.window.Event("submit", { bubbles: true, cancelable: true }));
await delay(20);
const request = ui.requests.find(r => r.method === "POST");
expect(form.querySelector("#repoUrl").classList.contains("is-invalid")).to.equal(true);
expect(request.payload).to.deep.equal({ repoUrl: "https://github.com/example/repository", repoId: "anonymous-id" });
expect(ui.errors).to.deep.equal([]);
});
it("debounces model updates and flushes them on blur", async function () {
ui = await browser("/profile", { "/api/user": { username: "tester" } });
const input = await ui.input("#terms", "private-name");
const form = ui.window.document.querySelector("form");
input.dispatchEvent(new ui.window.Event("blur"));
form.dispatchEvent(new ui.window.Event("submit", { bubbles: true, cancelable: true }));
await delay(20);
const request = ui.requests.find(r => r.method === "POST");
expect(request.payload.terms).to.deep.equal(["private-name"]);
expect(ui.errors).to.deep.equal([]);
});
it("keeps date values as local Dates and rejects out-of-range input", async function () {
ui = await browser("/conference/new", { "/api/user": { username: "tester" } });
const input = await ui.input("#startDate", "2027-02-15", "change");
const bound = input._field.binding.value;
expect(bound.getFullYear()).to.equal(2027);
expect(bound.getMonth()).to.equal(1);
expect(bound.getDate()).to.equal(15);
input.min = "2027-03-01";
input.dispatchEvent(new ui.window.Event("change", { bubbles: true }));
expect(input._field.validation.errors.min).to.equal(true);
expect(ui.errors).to.deep.equal([]);
});
it("renders hostile filenames as text and updates the explorer without losing the tree", async function () {
const filename = '{{constructor.constructor("window.probe=1")()}}.txt';
ui = await browser("/r/test/hello.txt", {
"/api/repo/test/files/": [{ name: filename, path: "", size: 1 }, { name: "constructor", path: "" }, { name: "index.js", path: "constructor", size: 2 }, { name: "hello.txt", path: "", size: 3 }],
});
const tree = ui.window.document.querySelector("tree");
expect(tree.textContent).to.include(filename).and.include("constructor/index.js");
expect(ui.window.probe).to.equal(undefined);
await ui.go("/r/test/constructor/index.js");
expect(ui.window.document.querySelector("tree")).to.equal(tree);
expect(ui.requests.some(r => r.url.pathname === "/api/repo/test/file/constructor/index.js")).to.equal(true);
await ui.go("/r/other/hello.txt");
expect(ui.requests.some(r => r.url.pathname === "/api/repo/other/options")).to.equal(true);
expect(ui.errors).to.deep.equal([]);
});
it("starts folders collapsed and opens the selected file's ancestors", async function () {
ui = await browser("/r/test/hello.txt", {
"/api/repo/test/files/": [
{ name: "hello.txt", path: "", size: 3 },
{ name: "src", path: "" },
{ name: "a.js", path: "src", size: 2 },
{ name: "b.js", path: "src", size: 2 },
{ name: "lazy", path: "" },
],
});
const folder = name => ui.window.document.querySelector(`tree a[data-path="/${name}"]`).parentElement;
expect(folder("src").classList.contains("open")).to.equal(false);
expect(folder("lazy").classList.contains("open")).to.equal(false);
expect(folder("src").querySelector("ul")).to.equal(null);
folder("src").querySelector("a").click();
await delay(10);
expect(folder("src").textContent).to.include("a.js");
folder("src").querySelector("a").click();
await delay(10);
expect(folder("src").querySelector("ul")).to.equal(null);
await ui.go("/r/test/src/a.js");
expect(folder("src").classList.contains("open")).to.equal(true);
expect(folder("lazy").classList.contains("open")).to.equal(false);
folder("lazy").querySelector("a").click();
await delay(10);
expect(ui.requests.filter(r => r.url.pathname === "/api/repo/test/files/" && r.url.searchParams.get("path") === "lazy")).to.have.length(1);
folder("lazy").querySelector("a").click();
await delay(10);
expect(ui.requests.filter(r => r.url.pathname === "/api/repo/test/files/" && r.url.searchParams.get("path") === "lazy")).to.have.length(1);
expect(ui.errors).to.deep.equal([]);
});
it("reuses query-only routes but reloads PR and gist data when their IDs change", async function () {
ui = await browser("/pr/first/");
await ui.go("/pr/second/");
await ui.go("/gist/first/");
await ui.go("/gist/second/");
expect(ui.requests.some(r => r.url.pathname === "/api/pr/second/content")).to.equal(true);
expect(ui.requests.some(r => r.url.pathname === "/api/gist/second/content")).to.equal(true);
const count = ui.requests.length;
await ui.go("/gist/second/?tab=comments");
expect(ui.requests).to.have.length(count);
expect(ui.errors).to.deep.equal([]);
});
it("keeps rendered HTML in an opaque sandbox when scripts are enabled", async function () {
ui = await browser("/r/test/report.html", { "/api/repo/test/file/report.html": '<h1>Report</h1><script>window.probe=1</script>' });
const frame = ui.window.document.querySelector("html-doc iframe");
expect(frame).not.to.equal(null);
expect(frame.srcdoc).to.include("Report");
expect(frame.getAttribute("sandbox")).not.to.include("allow-scripts");
ui.window.document.querySelector('[aria-label="Allow this document to run JavaScript"]').click();
await delay(20);
const enabled = ui.window.document.querySelector("html-doc iframe");
expect(enabled).not.to.equal(frame);
expect(enabled.getAttribute("sandbox")).to.include("allow-scripts").and.not.include("allow-same-origin");
expect(ui.window.probe).to.equal(undefined);
expect(ui.errors).to.deep.equal([]);
});
it("filters loaded dashboard rows through search and status controls", async function () {
ui = await browser("/dashboard", {
"/api/user": { username: "tester" },
"/api/user/anonymized_repositories": [
{ repoId: "alpha", status: "ready", source: { fullName: "owner/alpha" }, options: {}, pageView: 3 },
{ repoId: "beta", status: "ready", source: { fullName: "owner/beta" }, options: {}, pageView: 8 },
],
});
const rows = () => [...ui.window.document.querySelectorAll(".paper-table-row:not(.paper-table-skeleton)")];
expect(rows()).to.have.length(2);
await ui.input('input[type="search"]', "beta");
expect(rows()).to.have.length(1);
expect(rows()[0].textContent).to.include("beta");
await ui.input('input[type="search"]', "");
ui.window.document.querySelector("#status-ready").click();
await delay(10);
expect(rows()).to.have.length(0);
expect(ui.errors).to.deep.equal([]);
});
it("locks edit identifiers and reactively displays the anonymized PR preview", async function () {
ui = await browser("/pull-request-anonymize/test", {
"/api/pr/test": { source: { repositoryFullName: "owner/repo", pullRequestId: 1 }, options: { terms: ["Alice"] } },
"/api/pr/owner/repo/1": { pullRequest: { title: "Alice patch", body: "Alice body", comments: [] } },
"/api/anonymize-preview": request => ({ contents: request.payload.contents.map(text => text.replaceAll("Alice", "MASKED")) }),
});
expect(ui.window.document.querySelector("#pullRequestId").disabled).to.equal(true);
expect(ui.window.document.querySelector("#sourceUrl").disabled).to.equal(true);
await delay(260);
expect(ui.window.document.querySelector(".anonymize-preview-col").textContent).to.include("MASKED patch");
expect(ui.errors).to.deep.equal([]);
});
it("loads PDF pages, changes documents and releases the previous document", async function () {
ui = await browser("/faq");
const loaded = [], destroyed = [];
ui.window.pdfjsLib = { GlobalWorkerOptions: {}, getDocument({ url }) {
loaded.push(url);
return { promise: Promise.resolve({
numPages: 2,
getPage: async () => ({ getViewport: ({ scale }) => ({ width: 600 * scale, height: 800 * scale }), render: () => ({ promise: Promise.resolve() }) }),
destroy() { destroyed.push(url); },
}) };
} };
await ui.go("/r/test/first.pdf");
expect(ui.window.document.querySelectorAll(".pdf-viewer-page")).to.have.length(2);
await ui.go("/r/test/second.pdf");
expect(loaded).to.have.length(2);
expect(destroyed).to.deep.equal([loaded[0]]);
await ui.go("/faq");
expect(destroyed).to.deep.equal(loaded);
expect(ui.errors).to.deep.equal([]);
});
it("leaves server routes and external links to the browser", async function () {
ui = await browser();
for (const href of ["/w/test/", "/api/repo/test/file/a", "/github/login", "https://example.org/"]) {
const link = ui.window.document.createElement("a");
link.href = href;
ui.window.document.body.append(link);
const event = new ui.window.MouseEvent("click", { bubbles: true, cancelable: true, button: 0 });
link.dispatchEvent(event);
expect(event.defaultPrevented, href).to.equal(false);
}
expect(ui.app.router.currentRoute.value.path).to.equal("/");
});
});