Merge pull request #865 from tdurieux/fix/dashboard-ui-consistency
fix: unify app styling and compact mobile project lists
No files matched your search
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"core.min.js": "core.3b07189932.min.js",
|
||||
"vendor.min.js": "vendor.d87c0306f0.min.js",
|
||||
"vendor.min.js": "vendor.7e58c2ba4a.min.js",
|
||||
"mermaid.min.js": "mermaid.f848a72d16.min.js",
|
||||
"all.min.css": "all.5fbafcda4e.min.css",
|
||||
"all.min.css": "all.947ab87f1c.min.css",
|
||||
"markdown.min.js": "markdown.44f2e93820.min.js",
|
||||
"pdf.min.js": "pdf.4d9cc6ff49.min.js",
|
||||
"notebook.min.js": "notebook.c3144c18ae.min.js",
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
*/:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unsLine truncated
|
||||
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
|
||||
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
|
||||
@@ -101,6 +101,12 @@ body {
|
||||
|
||||
/* Two-step radius system: controls and surfaces. Chips/badges are pills. */
|
||||
--radius-control: 6px;
|
||||
--radius-button: 8px;
|
||||
--button-height: 36px;
|
||||
--button-font-size: 14px;
|
||||
--button-line-height: 20px;
|
||||
--button-padding-x: 13px;
|
||||
--button-compact-height: 32px;
|
||||
--radius-surface: 10px;
|
||||
|
||||
--font-serif: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
|
||||
@@ -344,6 +350,43 @@ a:hover {
|
||||
color: var(--canvas-bg-color) !important;
|
||||
}
|
||||
|
||||
.navbar .nav-support {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
order: 3;
|
||||
gap: 6px;
|
||||
min-height: 36px;
|
||||
margin-left: 12px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--color);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar .nav-support:hover {
|
||||
background: var(--hover-bg-color);
|
||||
border-color: var(--color);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.navbar .nav-support {
|
||||
order: 0;
|
||||
width: 44px;
|
||||
min-height: 44px;
|
||||
margin: 0 8px 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
.navbar .nav-support-label { display: none; }
|
||||
[class*="floatingchat-container-wrap"],
|
||||
iframe.floatingchat-container { display: none !important; }
|
||||
}
|
||||
|
||||
.navbar .user-chip {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
@@ -1212,10 +1255,20 @@ a:hover {
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: var(--button-height);
|
||||
padding: 7px var(--button-padding-x);
|
||||
font: 500 var(--button-font-size)/var(--button-line-height) var(--font-sans);
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
color: var(--btn-color);
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
background: var(--paper-card);
|
||||
border-radius: var(--radius-button);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1815,21 +1868,6 @@ notebook {
|
||||
border: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
.floatingchat-container-wrap {
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
bottom: 3px !important;
|
||||
height: 47px !important;
|
||||
right: 3px !important;
|
||||
}
|
||||
|
||||
.floating-chat-kofi-popup-iframe {
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
bottom: 55px !important;
|
||||
right: 10px !important;
|
||||
}
|
||||
|
||||
/* ===== Paper FAQ (two-col editorial) ===== */
|
||||
.paper-faq {
|
||||
max-width: 1180px;
|
||||
@@ -2960,16 +2998,6 @@ code {
|
||||
@media (max-width: 991px) {
|
||||
.sidebar-toggle { display: inline-flex; }
|
||||
|
||||
/* Move Ko-fi widget to the left to avoid clashing with the Files button */
|
||||
.floatingchat-container-wrap {
|
||||
left: 3px !important;
|
||||
right: auto !important;
|
||||
}
|
||||
.floating-chat-kofi-popup-iframe {
|
||||
left: 10px !important;
|
||||
right: auto !important;
|
||||
}
|
||||
|
||||
/* Sidebar becomes a slide-in drawer */
|
||||
.leftCol {
|
||||
position: fixed;
|
||||
@@ -3386,7 +3414,7 @@ code {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-detail-card .user-header h1 {
|
||||
.user-detail-card .user-header h2 {
|
||||
margin: 0;
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
@@ -4189,25 +4217,26 @@ code {
|
||||
.dark-mode .toast { box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
|
||||
|
||||
.paper-crumbs {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
color: var(--ink-muted);
|
||||
letter-spacing: 0.02em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.paper-crumbs a, .paper-crumbs .here {
|
||||
color: var(--color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.paper-page-title {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
font-size: clamp(2rem, 4.5vw, 3.2rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 10px 0 6px;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.035em;
|
||||
margin: 8px 0 6px;
|
||||
color: var(--color);
|
||||
}
|
||||
.paper-page-title em { color: var(--accent); font-style: italic; }
|
||||
.paper-page-title em { color: inherit; font-style: normal; }
|
||||
|
||||
.paper-page-lede {
|
||||
color: var(--ink-soft);
|
||||
font-size: 16px;
|
||||
@@ -4348,14 +4377,14 @@ code {
|
||||
}
|
||||
|
||||
.paper-field-label {
|
||||
display: block;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
color: var(--color);
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paper-switch-row {
|
||||
@@ -4738,11 +4767,12 @@ textarea::selection {
|
||||
padding: 8px 10px 6px;
|
||||
}
|
||||
.paper-field-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
color: var(--color);
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
@@ -5014,7 +5044,7 @@ textarea::selection {
|
||||
border-radius: var(--radius-control, 6px);
|
||||
padding: 8px 14px;
|
||||
}
|
||||
.btn.btn-outline-ink:hover { background: var(--hover-bg-color); color: var(--color); border-color: var(--color); }
|
||||
.btn.btn-outline-ink:hover { background: var(--hover-bg-color); color: var(--color); border-color: var(--ink-faint); }
|
||||
|
||||
.paper-page .dropdown-menu .dropdown-item-danger,
|
||||
.paper-page .dropdown-menu .dropdown-item-danger i { color: var(--status-error); }
|
||||
@@ -7130,11 +7160,20 @@ body {
|
||||
.auth-legacy h2 { font-size: 16px; margin: 0; }
|
||||
.auth-legacy p { font-size: 13px; }
|
||||
.source-url-label-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 16px; margin-bottom: 8px; }
|
||||
.source-url-label-row > .paper-field-label { margin: 0; }
|
||||
.source-url-label-row > .paper-field-label {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
color: var(--color);
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
.repo-access { margin: 0; }
|
||||
.repo-access-details { margin: 12px 0 24px; }
|
||||
.source-url-label-row .repo-access-switch { padding: 2px; }
|
||||
.source-url-label-row .repo-access-switch button { padding: 5px 8px; font-size: 11px; }
|
||||
.source-url-label-row .repo-access-switch button { padding: 6px 10px; font-size: 12px; line-height: 20px; }
|
||||
.repo-access-main { display: flex; align-items: center; gap: 10px; padding: 0 0 8px; flex-wrap: wrap; }
|
||||
.repo-access-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.repo-access-switch { display: inline-flex; gap: 3px; padding: 3px; border: 1px solid var(--border-color); border-radius: 7px; flex-wrap: wrap; }
|
||||
@@ -7165,3 +7204,450 @@ body {
|
||||
|
||||
.repo-access-controls { width: 100%; gap: 12px; }
|
||||
}
|
||||
|
||||
/* Bootstrap's muted utility must use the same accessible colors as our copy. */
|
||||
.text-muted { color: var(--ink-muted) !important; }
|
||||
.draft-status,
|
||||
.anonymize-review-note { font-size: 13px; color: var(--ink-muted); }
|
||||
.anonymize-review-note { padding: 12px 0; margin: 0; }
|
||||
.anonymize-preview-status { padding: 20px 32px; color: var(--ink-muted); }
|
||||
.anonymize-preview-status p { margin: 0 0 12px; }
|
||||
@media (max-width: 575px) {
|
||||
.dashboard-page .paper-page-lede { margin-bottom: 8px; }
|
||||
.dashboard-page .dashboard-heading-actions { gap: 4px; }
|
||||
.dashboard-page .dashboard-connections-link { min-height: 32px; }
|
||||
.dashboard-page .dashboard-filter-row { margin-top: 12px; }
|
||||
.anonymize-preview-status { padding: 16px; }
|
||||
}
|
||||
|
||||
/* Working screens: readable typography and clear groups on the warm canvas. */
|
||||
.paper-page .paper-page-title { font-size: 32px; }
|
||||
.paper-page.admin-page .paper-page-title { font-size: 28px; }
|
||||
.paper-page .paper-page-lede { font-size: 14px; line-height: 1.5; }
|
||||
.paper-settings { padding-top: 32px; }
|
||||
.paper-settings-body { grid-template-columns: 180px minmax(0, 1fr); gap: 40px; margin-top: 24px; padding-top: 24px; border-top: 0; }
|
||||
.paper-settings-main .paper-settings-section { background: var(--paper-card); border: 1px solid var(--border-color); border-radius: var(--radius-surface); padding: 20px; margin-bottom: 16px; }
|
||||
.paper-settings .paper-section-eyebrow,
|
||||
.anonymize-page .paper-section-eyebrow { font-family: var(--font-sans); font-size: 16px; font-weight: 600; color: var(--color); text-transform: none; letter-spacing: -0.01em; margin-bottom: 12px; }
|
||||
.paper-settings .paper-settings-toc-head { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; }
|
||||
.paper-settings-section .paper-section-copy { margin-top: 0; }
|
||||
.paper-settings .quota-row { gap: 24px; }
|
||||
.paper-settings .quota-header { flex-direction: column; align-items: flex-start; gap: 4px; }
|
||||
.paper-settings .quota-value { white-space: nowrap; }
|
||||
.paper-settings .paper-settings-footer { padding: 0 0 20px; }
|
||||
.paper-settings .form-control { font-size: 14px; border-color: var(--border-color); border-radius: var(--radius-control); }
|
||||
.paper-settings .form-check { padding-top: 8px; padding-bottom: 8px; }
|
||||
.paper-page .btn-ink, .anonymize-page .btn-ink { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.dark-mode .paper-page .btn-ink, .dark-mode .anonymize-page .btn-ink { color: #1A1815; }
|
||||
.paper-page .btn-ink:hover, .anonymize-page .btn-ink:hover { background: var(--accent); border-color: var(--accent); color: #fff; filter: brightness(.94); }
|
||||
.dark-mode .paper-page .btn-ink:hover, .dark-mode .anonymize-page .btn-ink:hover { color: #1A1815; }
|
||||
.paper-page .btn-ink:active, .anonymize-page .btn-ink:active { filter: brightness(.9); }
|
||||
input[type="date"], select { color-scheme: light; }
|
||||
.dark-mode input[type="date"], .dark-mode select { color-scheme: dark; }
|
||||
.draft-status { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; padding: 10px 0; }
|
||||
.draft-discard { padding: 0; border: 0; color: var(--accent); font: inherit; font-size: 13px; text-decoration: underline; background: transparent; cursor: pointer; }
|
||||
.conference-feedback { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px; margin-top: 16px; background: var(--paper-card); border: 1px solid var(--border-color); border-radius: var(--radius-surface); width: 100%; }
|
||||
|
||||
.paper-table { background: var(--paper-card); border: 1px solid var(--border-color); border-radius: var(--radius-surface); margin-top: 12px; }
|
||||
.paper-table .paper-table-head { font-family: var(--font-sans); text-transform: none; letter-spacing: 0; font-size: 12px; background: var(--paper-bg-alt); border-radius: var(--radius-surface) var(--radius-surface) 0 0; }
|
||||
.paper-table .paper-table-head, .paper-table .paper-table-row { padding-left: 16px; padding-right: 16px; }
|
||||
|
||||
/* Dashboard: a compact header, quiet controls, and fixed row actions. */
|
||||
.dashboard-page {
|
||||
--dashboard-space-xs: 4px;
|
||||
--dashboard-space-sm: 8px;
|
||||
--dashboard-space-md: 12px;
|
||||
--dashboard-space-lg: 16px;
|
||||
--dashboard-space-xl: 24px;
|
||||
padding-top: var(--dashboard-space-xl);
|
||||
}
|
||||
.project-icon-definitions { position: absolute; width: 0; height: 0; overflow: hidden; }
|
||||
.project-type-svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
|
||||
.project-empty-icon { width: 28px; height: 28px; color: var(--ink-faint); }
|
||||
.project-type-icon { display: inline-flex; color: var(--ink-muted); flex-shrink: 0; }
|
||||
.dashboard-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--dashboard-space-lg); }
|
||||
.dashboard-heading .paper-page-title { font-size: 28px; margin-bottom: var(--dashboard-space-xs); }
|
||||
.dashboard-heading .paper-page-lede { display: none; }
|
||||
.dashboard-heading .btn { min-height: 40px; display: inline-flex; gap: var(--dashboard-space-sm); align-items: center; }
|
||||
.dashboard-new-mobile { display: none; }
|
||||
.dashboard-quota { position: relative; margin: 0; padding: 0; border: 0; background: transparent; }
|
||||
.dashboard-quota > summary { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 5px 10px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--paper-card); color: var(--color); font-family: var(--font-sans); font-size: 12px; cursor: pointer; list-style: none; white-space: nowrap; }
|
||||
.dashboard-quota > summary::-webkit-details-marker { display: none; }
|
||||
.dashboard-quota > summary:hover, .dashboard-quota[open] > summary { background: var(--paper-bg-alt); border-color: var(--ink-faint); }
|
||||
.quota-summary-label { font-weight: 600; }
|
||||
.quota-summary-value { color: var(--ink-muted); font-variant-numeric: tabular-nums; }
|
||||
.quota-summary-ring { width: 18px; height: 18px; flex-shrink: 0; fill: none; stroke-width: 2.5; }
|
||||
.quota-ring-track { stroke: var(--border-color); }
|
||||
.quota-ring-fill { stroke: var(--accent); transform: rotate(-90deg); transform-origin: 50% 50%; }
|
||||
.quota-summary-ring.quota-warn .quota-ring-fill { stroke: var(--status-progress); }
|
||||
.quota-summary-ring.quota-danger .quota-ring-fill { stroke: var(--status-error); }
|
||||
.quota-ring-unlimited { stroke: var(--accent); stroke-width: 1.7; }
|
||||
.quota-summary-chevron { width: 14px; height: 14px; margin-left: 2px; color: var(--ink-muted); transition: transform .15s ease; }
|
||||
.dashboard-quota[open] .quota-summary-chevron { transform: rotate(180deg); }
|
||||
.dashboard-quota .quota-panel { position: absolute; z-index: 25; top: calc(100% + 8px); left: 0; width: 344px; max-width: calc(100vw - 36px); padding: 16px; border: 1px solid var(--border-color); border-radius: 12px; background: var(--paper-card); box-shadow: 0 12px 36px rgba(26,24,21,.16); }
|
||||
.quota-panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
||||
.quota-panel-heading h2 { font: 600 15px var(--font-sans); margin: 0; }
|
||||
.quota-panel-heading .project-close { margin: -8px -8px -8px 0; }
|
||||
.paper-page .quota-usage { display: grid; grid-template-columns: 1fr; gap: 16px; margin: 0; padding: 0; }
|
||||
.quota-usage .quota-item + .quota-item { padding-top: 16px; border-top: 1px solid var(--border-soft, var(--border-color)); }
|
||||
.quota-usage .quota-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.quota-usage .quota-label { display: inline-flex; align-items: center; gap: 8px; font: 500 12px var(--font-sans); letter-spacing: 0; text-transform: none; color: var(--ink-muted); }
|
||||
.quota-usage .quota-label .project-type-svg { width: 16px; height: 16px; }
|
||||
.quota-usage .quota-value { display: flex; align-items: baseline; gap: 4px; font: 12px var(--font-sans); font-variant-numeric: tabular-nums; color: var(--ink-muted); margin-bottom: 8px; }
|
||||
.quota-usage .quota-value strong { font-size: 20px; line-height: 1.2; font-weight: 600; color: var(--color); }
|
||||
.quota-percent, .quota-usage .quota-unlimited-tag { margin-left: auto; padding: 2px 6px; border-radius: 4px; background: var(--paper-bg-alt); font: 11px var(--font-sans); color: var(--ink-muted); white-space: nowrap; }
|
||||
.quota-warn > .quota-header .quota-percent { color: var(--status-progress); }
|
||||
.quota-danger > .quota-header .quota-percent { color: var(--status-error); }
|
||||
.quota-usage .quota-track { height: 5px; border: 0; background: var(--paper-bg-alt); }
|
||||
.quota-usage .quota-track .quota-fill { min-width: 0; background: var(--accent); }
|
||||
.quota-remaining { margin: 6px 0 0; color: var(--ink-muted); font-size: 11px; }
|
||||
.quota-remaining.quota-limit { color: var(--status-error); }
|
||||
.quota-loading { font-size: 12px; color: var(--ink-muted); margin: 12px 0 0; }
|
||||
.dashboard-page .dashboard-filter-row { display: flex; align-items: center; gap: var(--dashboard-space-md); padding: 0; margin: var(--dashboard-space-lg) 0 0; border: 0; background: transparent; }
|
||||
.dashboard-page .dashboard-filter-row .search-wrap { flex: 1 1 240px; min-width: 0; }
|
||||
.dashboard-page .dashboard-filter-row .form-control { height: 40px; padding: 8px 12px; font-size: 13px; background: var(--paper-card); }
|
||||
.dashboard-page .dashboard-filter-controls { display: flex; flex-wrap: nowrap; align-items: center; gap: var(--dashboard-space-sm); }
|
||||
.dashboard-extra-filters { display: flex; gap: var(--dashboard-space-sm); }
|
||||
.dashboard-filters-toggle { display: none; }
|
||||
.dashboard-page .dashboard-filter-row .btn-group .btn { border: 0; background: transparent; border-radius: 6px; height: 36px; padding: 6px 10px; }
|
||||
.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary { background: var(--accent-soft); color: var(--accent); }
|
||||
.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary:hover,
|
||||
.dashboard-page .dashboard-filter-row .btn-group .btn.btn-primary:active { background: var(--accent-soft) !important; color: var(--accent); }
|
||||
.dashboard-page .dashboard-filter-row .dropdown-toggle { height: 36px; padding: 6px 10px; }
|
||||
.dashboard-type-filter .project-type-svg { display: none; }
|
||||
.sort-direction { width: 14px; height: 14px; }
|
||||
.sort-direction.ascending { transform: rotate(180deg); }
|
||||
.dashboard-page .dashboard-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--dashboard-space-sm) var(--dashboard-space-md); margin: var(--dashboard-space-md) 0 0; padding: 0; }
|
||||
.dashboard-view-tabs { display: flex; align-items: center; gap: var(--dashboard-space-lg); }
|
||||
.dashboard-view-short { display: none; }
|
||||
.dashboard-view-tabs button { min-height: 36px; border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: transparent; font: inherit; font-size: 13px; color: var(--ink-muted); padding: 4px 0; }
|
||||
.dashboard-view-tabs button[aria-pressed="true"] { color: var(--color); border-bottom-color: var(--color); font-weight: 600; }
|
||||
.attention-count { padding: 1px 5px; margin-left: 4px; border-radius: 4px; background: var(--paper-bg-alt); font-size: 11px; font-variant-numeric: tabular-nums; }
|
||||
.attention-count.has-attention { color: var(--status-error); }
|
||||
.dashboard-count { margin-left: auto; font-size: 12px; color: var(--ink-muted); }
|
||||
.dashboard-active-filters { display: flex; align-items: center; flex-wrap: wrap; gap: var(--dashboard-space-sm); width: 100%; }
|
||||
.dashboard-page .filter-chip { display: inline-flex; font-family: var(--font-sans); font-size: 11px; text-transform: none; letter-spacing: 0; border-radius: 6px; }
|
||||
.dashboard-page .filter-chip .project-type-svg { width: 12px; height: 12px; margin-left: 4px; }
|
||||
.dashboard-page .filter-chip-close { min-width: 24px; min-height: 24px; display: inline-flex; align-items: center; justify-content: center; margin: -2px -4px -2px 2px; }
|
||||
.dashboard-page .btn-link-inline { color: var(--ink-muted); font-size: 12px; }
|
||||
.dashboard-page .paper-table-dashboard { margin-top: var(--dashboard-space-md); border-left: 0; border-right: 0; border-radius: 0; }
|
||||
.dashboard-page .paper-table-dashboard .paper-table-head, .dashboard-page .paper-table-dashboard .paper-table-row { grid-template-columns: minmax(280px,3fr) 180px 72px 148px 64px; gap: var(--dashboard-space-lg); padding: 8px 12px; }
|
||||
.dashboard-page .paper-table-dashboard .paper-table-head { border-radius: 0; }
|
||||
.dashboard-page .paper-table .cell-anon { gap: var(--dashboard-space-sm); }
|
||||
.dashboard-page .paper-table .anon-text { flex: 1; }
|
||||
.dashboard-page .paper-table .repo-name { font-family: var(--font-sans); font-size: 14px; font-weight: 600; }
|
||||
.dashboard-page .paper-table .anon-sub { display: flex; align-items: center; gap: var(--dashboard-space-sm); min-width: 0; }
|
||||
.anonymous-id { font-size: 12px; font-family: var(--font-mono); color: var(--ink-muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dashboard-page .paper-table .type-badge { font-family: var(--font-sans); letter-spacing: 0; font-size: 11px; text-transform: none; border-radius: 6px; }
|
||||
.dashboard-page .paper-table .status-sub { font-size: 12px; }
|
||||
.dashboard-page .paper-table .cell-conf { font-family: var(--font-sans); letter-spacing: 0; text-transform: none; }
|
||||
.dashboard-page .conf-tag .project-type-svg { width: 14px; height: 14px; }
|
||||
.dashboard-row-meta { display: contents; }
|
||||
.dashboard-page .cell-actions { gap: 0; align-items: center; }
|
||||
.copy-link-inline, .dashboard-page .btn-icon-dots, .project-close { display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; background: transparent; color: var(--ink-muted); padding: 0; width: 32px; height: 32px; min-width: 32px; cursor: pointer; }
|
||||
.copy-link-inline:hover, .copy-link-inline:focus-visible, .dashboard-page .btn-icon-dots:hover, .project-close:hover { background: var(--accent-soft); color: var(--accent); }
|
||||
.copy-link-inline:disabled { opacity: .5; cursor: wait; }
|
||||
.mobile-meta-icon { display: none; }
|
||||
.project-attention-action { display: inline-block; color: var(--accent); border: 0; background: transparent; padding: 0; font: inherit; font-size: 12px; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.project-attention-action { text-align: left; }
|
||||
.dashboard-page .cell-status .project-attention-action { flex-basis: 100%; }
|
||||
.project-details-row { width: 100%; border-bottom: 1px solid var(--border-color); }
|
||||
.project-details { padding: var(--dashboard-space-xl); background: var(--paper-bg-alt); }
|
||||
.project-details-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--dashboard-space-sm); }
|
||||
.project-details-heading h2 { font: 600 16px var(--font-sans); margin: 0; }
|
||||
.project-details-list { display: grid; grid-template-columns: 1fr 1fr; gap: var(--dashboard-space-md) var(--dashboard-space-xl); margin: 12px 0; }
|
||||
.project-details-list dt { font-size: 12px; color: var(--ink-muted); font-weight: 500; margin-bottom: 4px; }
|
||||
.project-details-list dd { margin: 0; font-size: 13px; overflow-wrap: anywhere; }
|
||||
.project-details-list code { color: var(--color); font-size: 12px; }
|
||||
.project-name-form { margin-top: var(--dashboard-space-lg); padding-top: var(--dashboard-space-lg); border-top: 1px solid var(--border-color); }
|
||||
.project-name-form label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 4px; }
|
||||
.project-name-form p, .project-progress-help { font-size: 12px; color: var(--ink-muted); margin-bottom: 8px; }
|
||||
.project-name-controls { display: flex; gap: var(--dashboard-space-sm); max-width: 560px; }
|
||||
.project-name-controls .form-control { min-width: 0; font-size: 14px; }
|
||||
.project-name-controls .btn { flex-shrink: 0; min-height: 40px; }
|
||||
.project-name-form .project-name-error { color: var(--status-error); }
|
||||
.project-details-button { min-height: 44px; padding: 0 8px; border: 0; border-radius: 6px; background: transparent; font: inherit; font-size: 12px; color: var(--ink-muted); cursor: pointer; }
|
||||
|
||||
/* Anonymization: distinct settings and reviewer panes, with a mobile switch. */
|
||||
.anonymize-page .anonymize-topbar-title { font-family: var(--font-sans); font-size: 28px; font-weight: 600; }
|
||||
.anonymize-topbar { padding: 16px 24px; }
|
||||
.anonymize-form-col { background: var(--paper-bg-alt); }
|
||||
.anonymize-form-col .paper-settings-main { padding: 20px 24px; }
|
||||
.anonymize-form-col .paper-settings-section { padding: 18px; }
|
||||
.anonymize-form-col .paper-settings-section:first-child { padding-top: 18px; }
|
||||
.anonymize-form-col .anonymize-submit-bar { background: var(--paper-bg-alt); padding: 12px 0; }
|
||||
.anonymize-preview-head .paper-eyebrow { font-family: var(--font-sans); font-size: 14px; font-weight: 600; color: var(--color); text-transform: none; letter-spacing: 0; }
|
||||
.anonymize-preview-col { background: var(--canvas-bg-color); padding: 24px; }
|
||||
.anonymize-preview-head { flex-wrap: wrap; gap: 6px 12px; margin-bottom: 16px; }
|
||||
.anonymize-preview-body { padding: 24px; }
|
||||
.anonymize-mobile-tabs, .mobile-preview-jump, .anonymize-preview-empty { display: none; }
|
||||
.anonymize-page .repo-access-note { font-size: 12px; }
|
||||
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.paper-settings-body { grid-template-columns: 1fr; gap: 18px; }
|
||||
.paper-settings-toc { padding: 0; }
|
||||
.paper-settings-toc-head { display: none; }
|
||||
.paper-table .paper-table-row { padding-left: 14px; padding-right: 14px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.paper-page .paper-page-title, .anonymize-page .paper-page-title { font-size: 24px; }
|
||||
.paper-settings { padding: 20px 16px 40px; }
|
||||
.paper-settings-main .paper-settings-section { padding: 16px; }
|
||||
.anonymize-topbar { padding: 12px 16px; }
|
||||
.anonymize-topbar .paper-crumbs { display: none; }
|
||||
.anonymize-mobile-tabs { display: flex; gap: 4px; padding: 8px 16px; border-bottom: 1px solid var(--border-color); background: var(--paper-card); }
|
||||
.anonymize-mobile-tabs button { flex: 1; font: inherit; font-size: 13px; padding: 10px; border: 0; border-radius: 6px; background: transparent; color: var(--ink-muted); }
|
||||
.anonymize-mobile-tabs button[aria-selected="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
||||
.anonymize-page:not(.mobile-preview-active) .anonymize-preview-col { display: none; }
|
||||
.mobile-preview-active .anonymize-form-col { display: none; }
|
||||
.mobile-preview-active .anonymize-preview-empty { display: block; padding: 24px 16px; color: var(--ink-muted); }
|
||||
.anonymize-form-col .paper-settings-main { padding: 16px; }
|
||||
.anonymize-form-col .anonymize-submit-bar { background: var(--paper-bg-alt); }
|
||||
.mobile-preview-jump { display: block; width: 100%; min-height: 44px; }
|
||||
.anonymize-preview-col { padding: 16px; }
|
||||
.anonymize-preview-body { padding: 16px; }
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
.paper-settings .quota-header { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width: 1100px) and (min-width: 901px) {
|
||||
.dashboard-page .dashboard-filter-row { flex-wrap: wrap; }
|
||||
.dashboard-page .dashboard-filter-row .search-wrap { flex-basis: 100%; }
|
||||
.dashboard-page .dashboard-filter-controls { width: 100%; justify-content: space-between; }
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.dashboard-page .paper-table.paper-table-dashboard .paper-table-row { gap: 4px 8px; padding: 8px 12px; }
|
||||
.dashboard-page .paper-table .cell-anon { flex: 1 1 calc(100% - 96px); min-width: 0; }
|
||||
.dashboard-page .paper-table .cell-actions { flex: 0 0 88px; }
|
||||
.dashboard-row-meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; order: 3; width: 100%; }
|
||||
.dashboard-page .dashboard-row-meta .cell-status { flex: 1 1 80px; flex-wrap: wrap; gap: 2px 8px; }
|
||||
.dashboard-page .dashboard-row-meta .cell-views, .dashboard-page .dashboard-row-meta .cell-expires { flex-shrink: 0; font-size: 12px; }
|
||||
.dashboard-page .cell-views::before, .dashboard-page .cell-expires::before { content: none; }
|
||||
.dashboard-page .mobile-meta-icon { display: inline-block; width: 14px; height: 14px; }
|
||||
.dashboard-page .dashboard-row-meta .status-sub-date { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
||||
.dashboard-page .dashboard-row-meta .status-sub-error, .dashboard-page .dashboard-row-meta .status-sub-warn { flex-basis: 100%; white-space: normal; overflow-wrap: anywhere; }
|
||||
.dashboard-page .dashboard-row-meta .status-sub::before { content: none; }
|
||||
.dashboard-page .project-attention-action { min-height: 44px; display: inline-flex; align-items: center; }
|
||||
.dashboard-page .paper-table .anon-sub .commit-hash, .dashboard-page .paper-table .anon-sub .conf-tag { display: none; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.container.dashboard-page { padding-top: 12px; }
|
||||
.dashboard-heading .paper-page-title { font-size: 24px; }
|
||||
.dashboard-heading-actions .btn-ink { min-height: 44px; }
|
||||
.dashboard-new-label { display: none; }
|
||||
.dashboard-new-mobile { display: inline; }
|
||||
.dashboard-quota > summary { min-height: 44px; gap: 6px; padding: 10px; font-size: 12px; line-height: 20px; }
|
||||
.dashboard-quota .quota-panel { max-height: calc(100dvh - 184px); overflow-y: auto; overscroll-behavior: contain; }
|
||||
.dashboard-page .dashboard-filter-row { flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
.dashboard-page .dashboard-filter-row .search-wrap { flex: 1 1 0; }
|
||||
.dashboard-page .dashboard-filter-row .form-control { height: 44px; }
|
||||
.dashboard-page .dashboard-filters-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; padding: 8px; min-height: 44px; background: transparent; border: 0; }
|
||||
.dashboard-page .dashboard-filter-controls { display: none; width: 100%; }
|
||||
.dashboard-page .dashboard-filter-controls.expanded { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.dashboard-page .dashboard-filter-controls .btn-group { width: 100%; display: flex; }
|
||||
.dashboard-page .dashboard-filter-controls .btn-group .btn { flex: 1; min-height: 44px; }
|
||||
.dashboard-extra-filters { display: flex; width: 100%; }
|
||||
.dashboard-page .dashboard-extra-filters .dropdown { flex: 1; min-width: 0; }
|
||||
.dashboard-page .dashboard-extra-filters .dropdown-toggle { width: 100%; height: 44px; }
|
||||
.dashboard-type-filter .type-filter-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
||||
.dashboard-type-filter .project-type-svg { display: inline-block; }
|
||||
.dashboard-page .dashboard-meta { margin-top: 8px; gap: 4px 8px; }
|
||||
.dashboard-view-tabs { gap: 16px; }
|
||||
.dashboard-view-tabs button { min-width: 44px; min-height: 44px; font-size: 12px; }
|
||||
.dashboard-count { font-size: 11px; }
|
||||
.dashboard-count { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
|
||||
.dashboard-view-long { display: none; }
|
||||
.dashboard-view-short { display: inline; }
|
||||
.dashboard-active-filters { display: contents; }
|
||||
.dashboard-page .filter-chip { font-size: 11px; padding: 0 8px; min-height: 44px; gap: 4px; }
|
||||
.dashboard-page .filter-chip-close { min-width: 44px; min-height: 44px; margin: 0; }
|
||||
.dashboard-page .btn-link-inline { min-height: 44px; font-size: 12px; }
|
||||
.dashboard-page .paper-table-dashboard {
|
||||
width: calc(100% + 36px);
|
||||
margin: 8px -18px 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.dashboard-page .paper-table .repo-name { font-size: 13px; }
|
||||
.project-details { padding: 16px 12px; }
|
||||
.project-details-list { grid-template-columns: 1fr; gap: 12px; }
|
||||
.project-name-controls { flex-wrap: wrap; }
|
||||
.project-name-controls .form-control { flex-basis: 100%; }
|
||||
.project-name-controls .btn { min-height: 44px; }
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.dashboard-attention-prefix { display: none; }
|
||||
.dashboard-attention-prefix + span { text-transform: capitalize; }
|
||||
}
|
||||
|
||||
/* Shared application styling, applied to every route and shared control. */
|
||||
.ui-icon-definitions { position: absolute; width: 0; height: 0; overflow: hidden; pointer-events: none; }
|
||||
.ui-icon { display: inline-block; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; vertical-align: -3px; }
|
||||
.ui-icon.sort-direction { width: 14px; height: 14px; }
|
||||
.pr-title-row { display: flex; align-items: flex-start; gap: 16px; }
|
||||
.pr-title-row .pr-title { flex: 1; min-width: 0; }
|
||||
.pr-title-row > .btn { flex-shrink: 0; }
|
||||
.ui-icon.record-type-icon { color: var(--ink-muted); }
|
||||
.btn .ui-icon, .dropdown-item .ui-icon { width: 16px; height: 16px; }
|
||||
.btn-icon-dots .ui-icon { width: 18px; height: 18px; }
|
||||
.paper-page .paper-page-title, .anonymize-page .paper-page-title { font-size: 28px; }
|
||||
.paper-page, .paper-faq { padding-top: 24px; }
|
||||
.paper-page .paper-page-lede { margin-bottom: 16px; overflow-wrap: anywhere; }
|
||||
.paper-page-title { overflow-wrap: anywhere; }
|
||||
.paper-page .form-control, .anonymize-page .form-control { min-height: 40px; font-family: var(--font-sans); font-size: 14px; border-radius: var(--radius-control); }
|
||||
.paper-page textarea.form-control, .anonymize-page textarea.form-control { height: auto; }
|
||||
/* Regular actions use 14px labels; table tools use the compact size. */
|
||||
.paper-page .btn, .anonymize-page .btn, .status-bar-actions .btn {
|
||||
min-height: var(--button-height); padding: 7px var(--button-padding-x);
|
||||
font: 500 var(--button-font-size)/var(--button-line-height) var(--font-sans);
|
||||
border-radius: var(--radius-button); gap: 6px;
|
||||
}
|
||||
.paper-page .btn.btn-sm, .anonymize-page .btn.btn-sm, .status-bar-actions .btn {
|
||||
min-height: var(--button-compact-height); padding: 5px 10px; font-size: 13px; border-radius: 6px;
|
||||
}
|
||||
.paper-page .btn-ink, .anonymize-page .btn-ink { min-height: 40px; padding: 9px 15px; }
|
||||
.btn > .ui-icon, .btn > .project-type-svg, .btn > i, .btn-hero > i { width: 16px; height: 16px; margin: 0 !important; flex-shrink: 0; }
|
||||
.btn.dropdown-toggle::after { margin-left: 0; }
|
||||
.btn:disabled, .btn.disabled { cursor: not-allowed; }
|
||||
.btn:disabled:hover, .btn.disabled:hover { filter: none; }
|
||||
.paper-page .btn.btn-link { background: transparent; border-color: transparent; }
|
||||
.auth-continue.btn { min-height: 44px; padding: 11px 16px !important; }
|
||||
.paper-page .btn.btn-icon, .anonymize-page .btn.btn-icon { width: 32px; height: 32px; min-width: 32px; min-height: 32px; padding: 0; }
|
||||
.paper-page .btn.btn-icon-dots { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; min-width: 32px; min-height: 32px; padding: 0; border: 0; background: transparent; color: var(--ink-muted); }
|
||||
.paper-page .btn-icon-dots:hover { background: var(--accent-soft); color: var(--accent); }
|
||||
.paper-page .btn-group .btn-primary:hover, .paper-page .btn-group .btn-primary:active { background: var(--accent-soft) !important; color: var(--accent); }
|
||||
.paper-page .paper-section-eyebrow { font-family: var(--font-sans); font-size: 16px; font-weight: 600; color: var(--color); text-transform: none; letter-spacing: 0; margin-bottom: 12px; }
|
||||
.paper-page .paper-field-label { font-family: var(--font-sans); font-size: 13px; letter-spacing: 0; }
|
||||
.paper-tabs .paper-tab { min-height: 44px; }
|
||||
.paper-pill, .status-pill { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; border-radius: 6px; }
|
||||
.paper-meta-rule { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; gap: 12px 24px; }
|
||||
.paper-meta-rule b:not(.commit-hash) { font-family: var(--font-sans); font-weight: 500; }
|
||||
.connections-grid { gap: 16px; }
|
||||
.connections-grid > section { background: var(--paper-card); border: 1px solid var(--border-color); border-radius: var(--radius-surface); padding: 20px; }
|
||||
.connections-heading { margin-bottom: 24px; }
|
||||
.status-page > .paper-settings-section, .claim-page .claim-form { padding: 20px; margin-top: 24px; border: 1px solid var(--border-color); border-radius: var(--radius-surface); background: var(--paper-card); }
|
||||
.claim-page .claim-form { max-width: 640px; }
|
||||
.status-page .anonymize-submit-bar { background: transparent; gap: 8px; flex-wrap: wrap; }
|
||||
.anonymize-landing { padding-top: 32px; }
|
||||
.paper-settings .quota-usage { grid-template-columns: repeat(3,minmax(0,1fr)); gap: 24px; }
|
||||
.paper-settings .quota-usage .quota-item + .quota-item { padding-top: 0; border-top: 0; padding-left: 16px; border-left: 1px solid var(--border-color); }
|
||||
.paper-settings .quota-usage .quota-header { flex-direction: row; align-items: center; flex-wrap: wrap; }
|
||||
.paper-settings .quota-usage .quota-value { white-space: normal; flex-wrap: wrap; }
|
||||
.paper-faq-hero { padding-bottom: 24px; margin-bottom: 24px; }
|
||||
.paper-faq-eyebrow, .paper-faq-toc-head { font-family: var(--font-sans); font-size: 12px; text-transform: none; letter-spacing: 0; margin-bottom: 8px; }
|
||||
.paper-faq-title { font: 600 28px/1.2 var(--font-sans); letter-spacing: -.025em; max-width: 720px; margin-bottom: 12px; }
|
||||
.paper-faq-title em { font-style: normal; }
|
||||
.paper-faq-lede { font-size: 14px; }
|
||||
.paper-faq-body { grid-template-columns: 180px minmax(0,1fr); gap: 40px; }
|
||||
.faq-section h4.faq-cat { font: 600 18px/1.4 var(--font-sans); padding-bottom: 12px; }
|
||||
.faq-section h4.faq-cat .ui-icon { display: none; }
|
||||
.faq-section .panel-group { margin-bottom: 32px; }
|
||||
.faq-section .panel-default .panel-title a { font: 500 15px/1.5 var(--font-sans); padding: 16px 0 16px 28px; min-height: 44px; }
|
||||
.faq-section .panel-default .panel-title a:after { top: 16px; }
|
||||
.paper-empty-title { font: 600 32px/1.2 var(--font-sans); letter-spacing: -.025em; }
|
||||
.paper-empty-title em { font-style: normal; }
|
||||
.btn-hero { justify-content: center; gap: 8px; min-height: 48px; padding: 13px 20px; font-size: 15px; line-height: 20px; background: var(--accent); border-color: var(--accent); color: #fff; border-radius: var(--radius-button); }
|
||||
.dark-mode .btn-hero { color: #1A1815; }
|
||||
.btn-hero:hover { background: var(--accent); color: #fff; filter: brightness(.94); }
|
||||
.dark-mode .btn-hero:hover { color: #1A1815; }
|
||||
.paper-table:not(.paper-table-dashboard) { border-left: 0; border-right: 0; border-radius: 0; }
|
||||
.paper-table:not(.paper-table-dashboard) .paper-table-head { border-radius: 0; }
|
||||
.paper-table .cell-anon { gap: 8px; }
|
||||
.paper-table .cell-anon .anon-text { flex: 1; }
|
||||
.paper-table-conferences .cell-anon .repo-name { font-family: var(--font-sans); font-weight: 600; }
|
||||
.paper-table .anon-sub > span, .paper-table .anon-sub > a { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.paper-table .anon-sub .ui-icon { width: 14px; height: 14px; }
|
||||
.paper-page .filter-chip { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; border-radius: 6px; }
|
||||
.paper-page .filter-chip-close .ui-icon { width: 12px; height: 12px; }
|
||||
.admin-nav { gap: 4px; border: 0; border-bottom: 1px solid var(--border-color); border-radius: 0; padding: 0 0 8px; background: transparent; }
|
||||
.admin-nav a { font-family: var(--font-sans); font-size: 13px; letter-spacing: 0; text-transform: none; border-radius: 6px; min-height: 40px; padding: 8px 12px; }
|
||||
.admin-nav a.active { background: var(--accent-soft); color: var(--accent); }
|
||||
.admin-nav .ui-icon { width: 16px; height: 16px; }
|
||||
.admin-filter-toolbar { background: transparent; border: 0; padding: 0; }
|
||||
.admin-filter-inline > label, .admin-filter-inline > span { font-family: var(--font-sans); text-transform: none; letter-spacing: 0; }
|
||||
.admin-summary .summary-total { font-family: var(--font-sans); font-size: 18px; font-weight: 600; }
|
||||
.admin-summary .summary-pill { font: 500 13px/20px var(--font-sans); min-height: 32px; padding: 5px 10px; border-radius: 6px; }
|
||||
.admin-summary .summary-pill.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
||||
.admin-summary .summary-pill .count { font-family: var(--font-sans); }
|
||||
.admin-active-chip { font-family: var(--font-sans); font-size: 12px; }
|
||||
.admin-active-chip button .ui-icon { width: 12px; height: 12px; }
|
||||
.user-detail-card .user-header h2 { font: 600 20px/1.4 var(--font-sans); }
|
||||
.user-detail-grid .detail-label, .paper-detail-grid .detail-label, .paper-progress .paper-progress-label, .status-page .paper-error-eyebrow, .errors-page .volume-head, .errors-page .errors-toolbar .select-wrap label, .overview-page .ov-panel-meta { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; }
|
||||
.paper-error-title, .overview-page .ov-chart-title, .overview-page .ov-panel-title { font: 600 16px/1.4 var(--font-sans); }
|
||||
.paper-error-head > .ui-icon, .paper-ratelimit-head > .ui-icon { margin-top: 3px; color: var(--status-error); }
|
||||
.paper-ratelimit-head > .ui-icon { color: var(--status-progress); }
|
||||
.errors-page .seg-tabs { border-radius: var(--radius-control); }
|
||||
.errors-page .seg-tabs button { min-height: 36px; padding: 7px 12px; font: 500 14px/20px var(--font-sans); }
|
||||
.errors-page .seg-tabs button.active { background: var(--accent-soft); color: var(--accent); }
|
||||
.overview-page .ov-chart-card, .overview-page .ov-panel-card, .errors-page .volume-chart, .errors-page .errors-list, .errors-page .kpi-card:hover { box-shadow: none; }
|
||||
.overview-page .ov-kpi-value, .overview-page .ov-daily-value, .errors-page .kpi-value, .q-card-count, .user-hero-name { font-family: var(--font-sans); font-size: 28px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
|
||||
.overview-page .ov-kpi-label, .overview-page .ov-daily-label, .errors-page .kpi-label, .q-card-name, .q-section-label, .user-hero-details .detail-label { font-family: var(--font-sans); font-size: 12px; letter-spacing: 0; text-transform: none; }
|
||||
.overview-page .ov-daily-card { background: var(--paper-card); box-shadow: none; border-radius: var(--radius-surface); }
|
||||
.overview-page .ov-daily-value, .dark-mode .overview-page .ov-daily-value { color: var(--accent); }
|
||||
.overview-page .ov-kpi-card, .errors-page .kpi-card { box-shadow: none; border-radius: var(--radius-surface); }
|
||||
.q-card { border-radius: var(--radius-surface); }
|
||||
.q-card.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
||||
.q-range-btns { border-radius: var(--radius-control); }
|
||||
.q-range-btns .btn, .q-job-detail-actions .btn { font-family: var(--font-sans); font-size: 13px; }
|
||||
.q-range-btns .btn.active { background: var(--accent-soft); color: var(--accent); }
|
||||
.btn-dark { border-radius: var(--radius-control); font-family: var(--font-sans); font-size: 12px; }
|
||||
@media (max-width: 900px) {
|
||||
.paper-settings .quota-usage { grid-template-columns: 1fr; gap: 16px; }
|
||||
.paper-settings .quota-usage .quota-item + .quota-item { padding-left: 0; border-left: 0; border-top: 1px solid var(--border-color); padding-top: 16px; }
|
||||
.paper-table:not(.paper-table-dashboard) .paper-table-row { gap: 4px 8px !important; padding: 8px 12px; }
|
||||
.paper-table:not(.paper-table-dashboard) .cell-anon { flex: 1 1 calc(100% - 52px); order: 1; }
|
||||
.paper-table:not(.paper-table-dashboard) .cell-actions { order: 2; flex: 0 0 44px; }
|
||||
.paper-table.paper-table-repos.has-bulk .paper-table-row > [role=cell]:first-child, .paper-table .admin-users-row > [role=cell]:first-child { flex: 0 0 24px; order: 0; }
|
||||
.paper-table.paper-table-repos.has-bulk .cell-anon, .paper-table .admin-users-row .cell-anon { flex-basis: calc(100% - 84px); }
|
||||
.paper-table:not(.paper-table-dashboard) .cell-anon .anon-sub { font-size: 12px; gap: 6px; }
|
||||
.paper-table:not(.paper-table-dashboard) .cell-status .status-sub { font-size: 12px; }
|
||||
.paper-table .cell-views::before, .paper-table .cell-expires::before { content: none; }
|
||||
.paper-table .cell-views .mobile-meta-icon, .paper-table .cell-expires .mobile-meta-icon { display: inline-block; width: 14px; height: 14px; }
|
||||
.paper-faq-body { grid-template-columns: 1fr; gap: 24px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.paper-page, .paper-faq { padding-top: 20px; }
|
||||
.paper-page .paper-page-title, .paper-page.admin-page .paper-page-title, .anonymize-page .paper-page-title, .paper-faq-title { font-size: 24px; }
|
||||
.paper-page .form-control, .anonymize-page .form-control { min-height: 44px; }
|
||||
.paper-page .btn, .anonymize-page .btn, .paper-settings-toc nav a, .connection-link, .draft-discard, .paper-faq-toc a { min-height: 44px; }
|
||||
.paper-page .btn, .anonymize-page .btn, .status-bar-actions .btn {
|
||||
min-height: 44px; padding: 11px 14px; font-size: 14px; line-height: 20px; white-space: normal;
|
||||
}
|
||||
.paper-page .btn.btn-sm, .anonymize-page .btn.btn-sm { min-height: 44px; padding: 11px 12px; font-size: 14px; }
|
||||
.paper-page .btn.btn-icon, .anonymize-page .btn.btn-icon, .paper-page .btn.btn-icon-dots, .copy-link-inline, .project-close { width: 44px; height: 44px; min-width: 44px; min-height: 44px; padding: 0; }
|
||||
.dashboard-page .dashboard-filter-row .btn-group .btn, .dashboard-page .dashboard-filter-row .dropdown-toggle { height: auto; min-height: 44px; font-size: 14px; padding: 11px 10px; }
|
||||
.dashboard-heading-actions .btn-ink { padding: 11px 16px; }
|
||||
.dashboard-page .dashboard-filters-toggle { padding: 11px 10px; }
|
||||
.pr-title-row .btn, .status-bar-actions .btn, .errors-page .btn-icon { min-width: 44px; min-height: 44px; padding: 0; }
|
||||
.status-bar-actions .btn { width: 44px; height: 44px; }
|
||||
.errors-page .seg-tabs button, .paper-tab { min-height: 44px; }
|
||||
.errors-page .seg-tabs button { padding: 11px 12px; }
|
||||
.status-bar-actions { flex-wrap: wrap; max-width: 100%; }
|
||||
.leftCol-close, .tree-search-clear { min-width: 44px; min-height: 44px; }
|
||||
.anonymize-landing { padding: 20px 18px; }
|
||||
.anonymize-landing .repo-access-switch button, .source-url-label-row .repo-access-switch button { min-height: 44px; padding: 11px 10px; font-size: 13px; }
|
||||
.paper-settings-body { margin-top: 16px; padding-top: 0; gap: 16px; }
|
||||
.paper-settings-toc nav { gap: 0 12px; }
|
||||
.paper-settings-toc nav a { display: inline-flex; align-items: center; }
|
||||
.paper-faq { padding-left: 18px; padding-right: 18px; }
|
||||
.paper-faq-toc nav { display: flex; flex-direction: row; flex-wrap: wrap; gap: 0 16px; }
|
||||
.paper-faq-toc a { display: inline-flex; align-items: center; padding: 8px 0; border-left: 0; border-bottom: 2px solid transparent; margin-left: 0; }
|
||||
.paper-faq-toc nav a:hover, .paper-faq-toc nav a.active { border-bottom-color: var(--accent); color: var(--accent); }
|
||||
.status-page > .paper-settings-section, .claim-page .claim-form { padding: 16px; margin-top: 16px; }
|
||||
.admin-nav { flex-direction: row; flex-wrap: nowrap; overflow-x: auto; gap: 4px; max-width: 100%; margin: 8px 0 16px; }
|
||||
.admin-nav a { flex-shrink: 0; min-height: 44px; }
|
||||
.admin-summary .summary-pill { min-height: 44px; padding: 11px 12px; font-size: 14px; }
|
||||
.admin-active-chip button { min-width: 44px; min-height: 44px; }
|
||||
.admin-filter-row { gap: 8px; }
|
||||
.admin-filter-row .admin-filter-inline { gap: 8px; flex-wrap: wrap; }
|
||||
.errors-page .kpi-grid { grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.q-cards { flex-direction: row; }
|
||||
.q-card { min-width: 130px; }
|
||||
.q-header-actions { flex-wrap: wrap; gap: 8px; }
|
||||
}
|
||||
@@ -58,6 +58,7 @@
|
||||
"path_not_defined": "The file path has not been resolved yet.",
|
||||
"invalid_file_path": "The requested file path is not valid.",
|
||||
"invalid_request": "The request is missing required fields or is malformed.",
|
||||
"invalid_project_name": "Use a project name of up to 100 characters without line breaks or control characters.",
|
||||
"no_file_selected": "Please select a file.",
|
||||
"file_not_found": "The requested file is not found.",
|
||||
"file_not_accessible": "The requested file is not accessible.",
|
||||
|
||||
|
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 62 KiB |
@@ -76,6 +76,8 @@
|
||||
<script src="/script/__VENDOR_JS__" defer></script>
|
||||
<script>
|
||||
window.addEventListener("load", function () {
|
||||
// Phones use the always-visible header link instead of an overlay.
|
||||
if (window.matchMedia("(max-width: 991px)").matches) return;
|
||||
var s = document.createElement("script");
|
||||
s.src = "https://storage.ko-fi.com/cdn/scripts/overlay-widget.js";
|
||||
s.onload = function () {
|
||||
|
||||
@@ -3,25 +3,25 @@
|
||||
<h1 class="paper-page-title">Conferences</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences" class="active"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</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" 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" 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" 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" 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>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1" :aria-pressed="!!(query?.ready)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1" :aria-pressed="!!(query?.preparing)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1" :aria-pressed="!!(query?.error)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1" :aria-pressed="!!(query?.expired)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1" :aria-pressed="!!(query?.removed)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Conferences" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -31,11 +31,11 @@
|
||||
<span class="admin-search-hint" v-if="!query?.search">/</span>
|
||||
</div>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1" aria-label="Previous page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg></button>
|
||||
<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" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage" aria-label="Next page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<span class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
|
||||
<span class="key">{{ chip?.label }}</span>
|
||||
<span>{{ chip?.value }}</span>
|
||||
<button type="button" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
<button type="button" @click="clearFilter(chip.key)" aria-label="Clear filter"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,15 +52,15 @@
|
||||
|
||||
<div class="paper-table paper-table-conferences w-100" role="table" aria-label="Conferences">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'name'}" @click="sortBy('name')">Conference <i class="fas" :class="sortIcon('name')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'name'}" @click="sortBy('name')">Conference <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('name')" :class="{ascending: sortIcon('name') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num">Repos</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'startDate'}" @click="sortBy('startDate')">Window <i class="fas" :class="sortIcon('startDate')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'startDate'}" @click="sortBy('startDate')">Window <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('startDate')" :class="{ascending: sortIcon('startDate') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(conference, index) in filteredConferences">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Conf</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Conference"><use href="#project-icon-conference"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="conference?.name" :href="safeUrl("/conference/" + (conference?.conferenceID))"></a>
|
||||
<div class="anon-sub">
|
||||
@@ -72,27 +72,27 @@
|
||||
<span class="status-dot" :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 v-text="fmt?.title(conference?.status)"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell">
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg>
|
||||
<a title="Show repositories in this conference" v-text="fmt?.number(conference?.repositories?.length || 0)" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"></a>
|
||||
</div>
|
||||
<div class="cell-expires" role="cell">{{ fmt?.date(conference?.startDate) }} – {{ fmt?.date(conference?.endDate) }}</div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> {{ fmt?.date(conference?.startDate) }} – {{ fmt?.date(conference?.endDate) }}</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")"><i class="fa fa-eye"></i> View</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"><i class="fas fa-code-branch"></i> View repositories</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/?conference=" + (conference?.conferenceID))"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> View repositories</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="conference?.status != 'removed'" @click.prevent="removeConference(conference)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="conference?.status != 'removed'" @click.prevent="removeConference(conference)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredConferences?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No conferences match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,10 +100,10 @@
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :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 <= 1"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :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 >= totalPage">Next <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>Per page</label>
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
<header class="errors-header">
|
||||
<h1 class="paper-page-title">Errors</h1>
|
||||
<div class="errors-actions">
|
||||
<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" @click="clearAll()"><i class="fas fa-trash"></i> Clear all</button>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export CSV</button>
|
||||
<button class="btn btn-sm btn-danger" type="button" @click="clearAll()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Clear all</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors" class="active"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<section class="kpi-grid">
|
||||
@@ -78,7 +78,7 @@
|
||||
<button type="button" :class="{active: query?.bucket === 'info'}" @click="setBucket('info')">Info</button>
|
||||
</div>
|
||||
<div class="search-wrap">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<svg class="ui-icon search-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>
|
||||
<input type="search" class="form-control" placeholder="code:repo_not_found module:route status:>=400" autocomplete="off" v-field="{ state: viewState, set: value => { query.search = value }, value: query?.search, form: null, options: {} }">
|
||||
<span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()" aria-label="Refresh"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
||||
</form>
|
||||
|
||||
<div class="admin-empty" v-if="!visible?.length">No errors captured.</div>
|
||||
@@ -164,8 +164,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<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" @click="copyJson(row)"><i class="fas fa-clipboard"></i> Copy JSON</button>
|
||||
<button class="btn btn-sm" type="button" title="Copy a curl that reproduces the request" @click="copyCurl(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Copy curl</button>
|
||||
<button class="btn btn-sm" type="button" @click="copyJson(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-clipboard"></use></svg> Copy JSON</button>
|
||||
<span class="copy-hint" v-if="copyHint">{{ copyHint }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
</div>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/" class="active"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-empty" v-if="loading">Loading overview…</div>
|
||||
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><i class="fas fa-exclamation-triangle"></i> {{ error }}</div>
|
||||
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ error }}</div>
|
||||
|
||||
<div v-if="data">
|
||||
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
</div>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues" class="active"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<!-- Queue overview cards -->
|
||||
<div class="q-cards">
|
||||
<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" role="button" tabindex="0" :aria-pressed="selectedQueue === q.key" @keydown.enter.prevent="selectQueue(q.key)" @keydown.space.prevent="selectQueue(q.key)" v-for="(q, index) in queueList" :class="{selected: selectedQueue == q?.key, paused: q?.paused}" @click="selectQueue(q.key)">
|
||||
<div class="q-card-head">
|
||||
<span class="q-dot" :class="{'q-dot-red': q?.paused || q?.counts?.failed > 0}"></span>
|
||||
<span class="q-card-name" v-text="q?.label"></span>
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
|
||||
<div class="q-toast-error" v-if="actionError" @click="actionError = null">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ actionError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> {{ actionError }}
|
||||
</div>
|
||||
|
||||
<!-- Jobs table -->
|
||||
@@ -109,7 +109,7 @@
|
||||
<input type="checkbox" v-field="{ state: viewState, set: value => { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn btn-sm" type="button" title="Refresh now" @click="refreshNow()"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()" aria-label="Refresh"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
||||
</div>
|
||||
|
||||
<table class="q-table" v-if="filteredJobs().length > 0">
|
||||
@@ -131,7 +131,7 @@
|
||||
<span class="q-delay-hint" v-if="job?._state == 'delayed' && job?.delayUntil" v-text="delayCountdown(job?.delayUntil)"></span>
|
||||
</td>
|
||||
<td class="q-cell-id">
|
||||
<i class="fas fa-chevron-right q-chevron" :class="{'q-chevron-open': expanded[job?.id]}"></i>
|
||||
<svg class="ui-icon q-chevron" :class="{'q-chevron-open': expanded[job?.id]}" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
|
||||
<a target="_blank" v-text="'job:' + fmt.limitTo(job?.id, 6)" @click="$event.stopPropagation()" :href="safeUrl("/r/" + (job?.id))"></a>
|
||||
</td>
|
||||
<td class="q-cell-payload">
|
||||
@@ -147,8 +147,8 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="q-cell-actions">
|
||||
<button class="btn btn-sm" title="Retry" v-if="job?._state == 'failed'" @click="retryJob(job); $event.stopPropagation()"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm" title="Remove" @click="removeJob(job); $event.stopPropagation()"><i class="fas fa-trash-alt"></i></button>
|
||||
<button class="btn btn-sm btn-icon" title="Retry" v-if="job?._state == 'failed'" @click="retryJob(job); $event.stopPropagation()" aria-label="Retry job"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
|
||||
<button class="btn btn-sm btn-icon" title="Remove" aria-label="Remove job" @click="removeJob(job); $event.stopPropagation()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="q-detail-row" v-if="expanded[job?.id]">
|
||||
@@ -205,9 +205,9 @@
|
||||
<pre class="q-error-stack" v-for="(stack, index) in job?.stacktrace" :key="index"><code v-text="stack"></code></pre>
|
||||
</div>
|
||||
<div class="q-job-detail-actions">
|
||||
<button class="btn btn-sm" v-if="job?._state == 'failed'" @click="retryJob(job)"><i class="fas fa-sync"></i> Retry</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" :href="safeUrl("/r/" + (job?.id))"><i class="fas fa-external-link-alt"></i> View repo</a>
|
||||
<button class="btn btn-sm" v-if="job?._state == 'failed'" @click="retryJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Retry</button>
|
||||
<button class="btn btn-sm" @click="removeJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</button>
|
||||
<a class="btn btn-sm" target="_blank" :href="safeUrl("/r/" + (job?.id))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg> View repo</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -216,7 +216,7 @@
|
||||
</table>
|
||||
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg>
|
||||
<span v-if="!query?.search">No jobs in the {{ selectedQueue }} queue.</span>
|
||||
<span v-if="query?.search">No jobs match the current filters.</span>
|
||||
</div>
|
||||
|
||||
@@ -3,26 +3,26 @@
|
||||
<h1 class="paper-page-title">Repositories</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories" class="active"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
|
||||
<span class="summary-meta">{{ fmt?.humanFileSize(totalSize) }} on disk</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" 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" 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" 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" 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>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: query?.ready}" @click="query.ready = !query.ready; query.page = 1" :aria-pressed="!!(query?.ready)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: query?.preparing}" @click="query.preparing = !query.preparing; query.page = 1" :aria-pressed="!!(query?.preparing)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing') + statusCountFor('download')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: query?.error}" @click="query.error = !query.error; query.page = 1" :aria-pressed="!!(query?.error)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: query?.expired}" @click="query.expired = !query.expired; query.page = 1" :aria-pressed="!!(query?.expired)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired') + statusCountFor('expiring')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: query?.removed}" @click="query.removed = !query.removed; query.page = 1" :aria-pressed="!!(query?.removed)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed') + statusCountFor('removing')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -35,11 +35,11 @@
|
||||
<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 => { 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" v-field="{ state: viewState, set: value => { query.conference = value }, value: query?.conference, form: null, options: {} }"></span>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1" aria-label="Previous page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg></button>
|
||||
<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" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage" aria-label="Next page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<span class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
|
||||
<span class="key">{{ chip?.label }}</span>
|
||||
<span>{{ chip?.value }}</span>
|
||||
<button type="button" aria-label="Remove filter" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
<button type="button" aria-label="Remove filter" @click="clearFilter(chip.key)"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,8 +57,8 @@
|
||||
|
||||
<div class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<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" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="clearSelection()">Clear</button>
|
||||
</div>
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <i class="fas" :class="sortIcon('source.repositoryName')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <i class="fas" :class="sortIcon('pageView')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <i class="fas" :class="sortIcon('anonymizeDate')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('source.repositoryName')" :class="{ascending: sortIcon('source.repositoryName') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num"><button type="button" class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('pageView')" :class="{ascending: sortIcon('pageView') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('anonymizeDate')" :class="{ascending: sortIcon('anonymizeDate') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error', 'row-selected': selected[repo?.repoId]}">
|
||||
@@ -78,11 +78,11 @@
|
||||
<input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value => { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a v-text="repo?.source?.repositoryName" :href="safeUrl("https://github.com/" + (repo?.source?.repositoryName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
<a v-text="repo?.source?.repositoryName" :href="safeUrl("https://github.com/" + (repo?.source?.repositoryName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,31 +93,31 @@
|
||||
</span>
|
||||
<span class="status-sub" v-if="repo?.statusMessage" v-text="fmt?.statusMsg(repo?.statusMessage)" :title="(repo?.statusMessage)"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView || 0)"></div>
|
||||
<div class="cell-expires" role="cell" v-text="fmt?.humanTime(repo?.anonymizeDate)"></div>
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> <span v-text="fmt?.number(repo?.pageView || 0)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> <span v-text="fmt?.humanTime(repo?.anonymizeDate)"></span></div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><i class="fa fa-eye"></i> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><i class="fas fa-globe"></i> View page</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg> View page</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>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</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>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> View status message</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<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="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No repositories match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,12 +126,12 @@
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1">
|
||||
<i class="fas fa-chevron-left"></i> Previous
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous
|
||||
</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" aria-label="Page" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage">
|
||||
Next <i class="fas fa-chevron-right"></i>
|
||||
Next <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
|
||||
@@ -3,31 +3,31 @@
|
||||
<h1 class="paper-page-title">{{ userInfo?.username || 'User' }}</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users" class="active"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="user-detail-card" v-if="userInfo">
|
||||
<div class="user-header">
|
||||
<img width="56" height="56" v-if="userInfo?.photo" :src="safeUrl((userInfo?.photo))">
|
||||
<div>
|
||||
<h1>
|
||||
<h2>
|
||||
{{ userInfo?.username }}
|
||||
<span class="status-dot-wrap">
|
||||
<span class="status-dot" :class="{'status-ready': userInfo?.status == 'active', 'status-removed': userInfo?.status != 'active'}"></span>
|
||||
<span v-text="fmt?.title(userInfo?.status)"></span>
|
||||
</span>
|
||||
<span class="type-badge type-repo" v-if="userInfo?.isAdmin">Admin</span>
|
||||
</h1>
|
||||
</h2>
|
||||
<div class="user-actions" style="margin-top: 4px;">
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.status !== 'banned'" @click="banUser()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm" v-if="userInfo?.status === 'banned' || userInfo?.status === 'removed'" @click="activateUser()"><i class="fas fa-check-circle"></i> Activate</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" v-if="userInfo?.isAdmin" @click="demoteUser()"><i class="fas fa-user-minus"></i> Remove admin</button>
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.status !== 'banned'" @click="banUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-ban"></use></svg> Ban</button>
|
||||
<button class="btn btn-sm" v-if="userInfo?.status === 'banned' || userInfo?.status === 'removed'" @click="activateUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Activate</button>
|
||||
<button class="btn btn-sm" v-if="!userInfo?.isAdmin" @click="promoteUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user-shield"></use></svg> Promote to admin</button>
|
||||
<button class="btn btn-sm text-danger" v-if="userInfo?.isAdmin" @click="demoteUser()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user-minus"></use></svg> Remove admin</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
{{ showRepos ? 'Hide' : 'Show' }}
|
||||
</button>
|
||||
<button class="btn btn-sm ml-1" @click="getGitHubRepositories()">
|
||||
<i class="fas fa-sync"></i> Refresh
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,13 +75,13 @@
|
||||
<div class="paper-table w-100" style="margin-top: 10px;">
|
||||
<div class="paper-table-row" style="grid-template-columns: 1fr 160px;" v-for="(repo, index) in userInfo?.repositories">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<span class="repo-name" v-text="repo?.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<i class="fas fa-database"></i> {{ fmt?.humanFileSize(repo?.size) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-database"></use></svg> {{ fmt?.humanFileSize(repo?.size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header" v-if="userInfo && userInfo?.isAdmin && user && user?.username == userInfo?.username">
|
||||
<h2><i class="fas fa-key"></i> API tokens</h2>
|
||||
<h2><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-key"></use></svg> API tokens</h2>
|
||||
<span class="section-count">{{ tokens?.length }}</span>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
<form class="d-flex" style="gap: 8px; margin-bottom: 12px;" v-form="viewState" @submit.prevent="submitForm($event, () => { createToken() })">
|
||||
<input type="text" class="form-control" placeholder="Token name (e.g. dev-laptop)" required v-field="{ state: viewState, set: value => { 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"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-plus"></use></svg> Generate</button>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-warning" role="alert" v-if="tokenForm?.plaintext">
|
||||
@@ -119,28 +119,28 @@
|
||||
<div role="cell" v-text="fmt?.humanTime(t?.createdAt)"></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">
|
||||
<button class="btn btn-sm text-danger" title="Revoke" @click="revokeToken(t)"><i class="fas fa-trash-alt"></i></button>
|
||||
<button class="btn btn-sm text-danger btn-icon" title="Revoke" aria-label="Revoke token" @click="revokeToken(t)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="!tokens?.length">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No tokens yet.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section-header">
|
||||
<h2><i class="fas fa-code-branch"></i> Anonymized repositories</h2>
|
||||
<h2><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Anonymized repositories</h2>
|
||||
<span class="section-count">{{ repositories?.length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ fmt?.number(repositories?.length) }}</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" 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" 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" 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" 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>
|
||||
<button type="button" class="summary-pill ok" title="Toggle ready filter" :class="{active: filters?.status?.ready === false}" @click="filters.status.ready = !filters.status.ready" :aria-pressed="!!(filters?.status?.ready === false)">Ready <span class="count">{{ fmt?.number(statusCountFor('ready')) }}</span></button>
|
||||
<button type="button" class="summary-pill warn" title="Toggle preparing filter" :class="{active: filters?.status?.preparing === false}" @click="filters.status.preparing = !filters.status.preparing" :aria-pressed="!!(filters?.status?.preparing === false)">Preparing <span class="count">{{ fmt?.number(statusCountFor('preparing')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" title="Toggle errored filter" :class="{active: filters?.status?.error === false}" @click="filters.status.error = !filters.status.error" :aria-pressed="!!(filters?.status?.error === false)">Errored <span class="count">{{ fmt?.number(statusCountFor('error')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle expired filter" :class="{active: filters?.status?.expired === false}" @click="filters.status.expired = !filters.status.expired" :aria-pressed="!!(filters?.status?.expired === false)">Expired <span class="count">{{ fmt?.number(statusCountFor('expired')) }}</span></button>
|
||||
<button type="button" class="summary-pill" title="Toggle removed filter" :class="{active: filters?.status?.removed === false}" @click="filters.status.removed = !filters.status.removed" :aria-pressed="!!(filters?.status?.removed === false)">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Repositories" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -149,14 +149,14 @@
|
||||
<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 => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" title="Export current view to CSV" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<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" @click="bulkRemoveCache()"><i class="fas fa-broom"></i> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="bulkRefresh()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force refresh</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkRemoveCache()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</button>
|
||||
<button class="btn btn-sm" type="button" @click="clearSelection()">Clear</button>
|
||||
</div>
|
||||
|
||||
@@ -165,10 +165,10 @@
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <i class="fas" :class="sortIcon('source.repositoryName')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader" class="num"><span class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <i class="fas" :class="sortIcon('pageView')"></i></span></div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <i class="fas" :class="sortIcon('anonymizeDate')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'source.repositoryName'}" @click="sortBy('source.repositoryName')">Repository <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('source.repositoryName')" :class="{ascending: sortIcon('source.repositoryName') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num"><button type="button" class="sortable" :class="{active: query?.sort == 'pageView'}" @click="sortBy('pageView')">Views <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('pageView')" :class="{ascending: sortIcon('pageView') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'anonymizeDate'}" @click="sortBy('anonymizeDate')">Anonymized <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('anonymizeDate')" :class="{ascending: sortIcon('anonymizeDate') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error', 'row-selected': selected[repo?.repoId]}">
|
||||
@@ -176,11 +176,11 @@
|
||||
<input type="checkbox" aria-label="Select repository" v-field="{ state: viewState, set: value => { selected[repo.repoId] = value }, value: selected[repo?.repoId], form: null, options: {} }">
|
||||
</div>
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" target="_blank" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId))"></a>
|
||||
<div class="anon-sub">
|
||||
<a v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <i class="fas fa-chalkboard-teacher"></i> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
<a v-text="repo?.source?.fullName" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/")"></a><span v-if="repo?.options?.update"> · <a v-text="repo?.source?.branch" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.branch))"></a></span><span v-if="!repo?.options?.update"> · @<a v-text="repo?.source?.commit?.substring(0, 8)" :href="safeUrl("https://github.com/" + (repo?.source?.fullName) + "/tree/" + (repo?.source?.commit))"></a></span><span v-if="repo?.conference"> · <svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ repo?.conference }}</span><span> · {{ fmt?.humanFileSize(repo?.size?.storage) }}</span><span> · {{ fmt?.number(repo?.options?.terms?.length) }} terms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,26 +196,26 @@
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><i class="far fa-edit"></i> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><i class="fa fa-eye"></i> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><i class="fas fa-globe"></i> View page</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View repo</a>
|
||||
<a class="dropdown-item" target="_self" v-if="repo?.options?.page && repo?.status == 'ready'" :href="safeUrl("/w/" + (repo?.repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg> View page</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>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><i class="fas fa-sync"></i> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><i class="fas fa-check-circle"></i> Enable</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>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'ready' || repo?.status == 'error'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force update</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.status == 'removed'" @click.prevent="updateRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Enable</a>
|
||||
<a class="dropdown-item" href="#" v-show="repo?.statusMessage" @click.prevent="showStatusMessage(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> View status message</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<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="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><i class="fas fa-trash-alt"></i> Remove</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="removeCache(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-broom"></use></svg> Remove cache</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="repo?.status == 'ready'" @click.prevent="removeRepository(repo)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No repositories match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
<h1 class="paper-page-title">Users</h1>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/"><i class="fas fa-tachometer-alt"></i> Overview</a>
|
||||
<a href="/admin/repositories"><i class="fas fa-code-branch"></i> Repositories</a>
|
||||
<a href="/admin/users" class="active"><i class="fas fa-users"></i> Users</a>
|
||||
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
|
||||
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
|
||||
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
|
||||
<a href="/admin/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
||||
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
||||
<a href="/admin/users" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
||||
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
||||
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
||||
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
||||
</nav>
|
||||
|
||||
<div class="admin-summary">
|
||||
<span class="summary-total">{{ total >= 0 ? fmt.number(total) : '…' }}</span>
|
||||
<span class="summary-pill ok" :class="{active: query?.status == 'active'}" @click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></span>
|
||||
<span class="summary-pill error" :class="{active: query?.status == 'banned'}" @click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></span>
|
||||
<span class="summary-pill" :class="{active: query?.status == 'removed'}" @click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></span>
|
||||
<button type="button" class="summary-pill ok" :class="{active: query?.status == 'active'}" @click="query.status = query.status == 'active' ? '' : 'active'; query.page = 1" :aria-pressed="!!(query?.status == 'active')">Active <span class="count">{{ fmt?.number(statusCountFor('active')) }}</span></button>
|
||||
<button type="button" class="summary-pill error" :class="{active: query?.status == 'banned'}" @click="query.status = query.status == 'banned' ? '' : 'banned'; query.page = 1" :aria-pressed="!!(query?.status == 'banned')">Banned <span class="count">{{ fmt?.number(statusCountFor('banned')) }}</span></button>
|
||||
<button type="button" class="summary-pill" :class="{active: query?.status == 'removed'}" @click="query.status = query.status == 'removed' ? '' : 'removed'; query.page = 1" :aria-pressed="!!(query?.status == 'removed')">Removed <span class="count">{{ fmt?.number(statusCountFor('removed')) }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" style="margin: 8px 0;" v-if="fetchError">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ fetchError }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ fetchError }}
|
||||
</div>
|
||||
|
||||
<form class="w-100 admin-filter-toolbar" aria-label="Users" accept-charset="UTF-8" v-form="viewState">
|
||||
@@ -32,11 +32,11 @@
|
||||
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value => { query.role = value }, value: query?.role, form: null, options: {} }"><option value="">Any</option><option value="admin">Admin</option></select>
|
||||
</span>
|
||||
<span class="admin-filter-spacer"></span>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><i class="fas fa-file-csv"></i> Export</button>
|
||||
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export</button>
|
||||
<span class="admin-filter-inline" aria-label="Pagination">
|
||||
<button class="btn btn-sm" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.max(1, query.page - 1)" :disabled="query?.page <= 1" aria-label="Previous page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg></button>
|
||||
<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" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage"><i class="fas fa-chevron-right"></i></button>
|
||||
<button class="btn btn-sm btn-icon" type="button" @click="query.page = Math.min(totalPage, query.page + 1)" :disabled="query?.page >= totalPage" aria-label="Next page"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<span class="admin-active-chip" v-for="(chip, index) in chips" :key="chip?.key">
|
||||
<span class="key">{{ chip?.label }}</span>
|
||||
<span>{{ chip?.value }}</span>
|
||||
<button type="button" @click="clearFilter(chip.key)"><i class="fas fa-times"></i></button>
|
||||
<button type="button" @click="clearFilter(chip.key)" aria-label="Clear filter"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<div class="bulk-bar" v-if="selectedCount() > 0">
|
||||
<span><strong>{{ selectedCount() }}</strong> selected</span>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkBan()"><i class="fas fa-ban"></i> Ban</button>
|
||||
<button class="btn btn-sm text-danger" type="button" @click="bulkBan()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-ban"></use></svg> Ban</button>
|
||||
<button class="btn btn-sm" type="button" @click="selected = {}; allSelected = false">Clear</button>
|
||||
</div>
|
||||
|
||||
@@ -62,9 +62,9 @@
|
||||
<div role="columnheader" style="width: 28px;">
|
||||
<input type="checkbox" aria-label="Select all on page" @click="selectAllOnPage()" :checked="allSelected">
|
||||
</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'username'}" @click="sortBy('username')">User <i class="fas" :class="sortIcon('username')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'username'}" @click="sortBy('username')">User <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('username')" :class="{ascending: sortIcon('username') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader" class="num">Repos</div>
|
||||
<div role="columnheader"><span class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <i class="fas" :class="sortIcon('status')"></i></span></div>
|
||||
<div role="columnheader"><button type="button" class="sortable" :class="{active: query?.sort == 'status'}" @click="sortBy('status')">Status <svg class="ui-icon sort-direction" aria-hidden="true" v-if="sortIcon('status')" :class="{ascending: sortIcon('status') === 'fa-arrow-up'}"><use href="#project-icon-arrow"></use></svg></button></div>
|
||||
<div role="columnheader">Role</div>
|
||||
<div role="columnheader" aria-label="Actions"></div>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell">
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg>
|
||||
<a title="Show this user's repositories" v-text="fmt?.number((u?.repoCount || 0))" :href="safeUrl("/admin/?owner=" + (u?.username))"></a>
|
||||
</div>
|
||||
<div class="cell-status" role="cell">
|
||||
@@ -95,20 +95,20 @@
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/users/" + (u?.username))"><i class="far fa-eye"></i> View details</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/?owner=" + (u?.username))"><i class="fas fa-code-branch"></i> View repositories</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/users/" + (u?.username))"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View details</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/admin/?owner=" + (u?.username))"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> View repositories</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="u?.status == 'active'" @click.prevent="banUser(u)"><i class="fas fa-ban"></i> Ban</a>
|
||||
<a class="dropdown-item" href="#" v-show="u?.status == 'removed' || u?.status == 'banned'" @click.prevent="activateUser(u)"><i class="fas fa-check-circle"></i> Activate</a>
|
||||
<a class="dropdown-item text-danger" href="#" v-show="u?.status == 'active'" @click.prevent="banUser(u)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-ban"></use></svg> Ban</a>
|
||||
<a class="dropdown-item" href="#" v-show="u?.status == 'removed' || u?.status == 'banned'" @click.prevent="activateUser(u)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Activate</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredUsers?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No users match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,10 +116,10 @@
|
||||
<div class="admin-toolbar" style="justify-content: space-between; border-bottom: none;">
|
||||
<span style="font-size: 12px; color: var(--ink-muted);">{{ fmt?.number(total) }} results</span>
|
||||
<div class="pagination-compact" v-if="totalPage > 1">
|
||||
<button class="btn btn-sm" @click="query.page = Math.max(1, query.page - 1)" :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 <= 1"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-left"></use></svg> Previous</button>
|
||||
<input type="number" class="form-control form-control-sm" min="1" style="width: 56px;" v-field="{ state: viewState, set: value => { query.page = value }, value: query?.page, form: null, options: {} }" :max="(totalPage)">
|
||||
<span>of {{ totalPage }}</span>
|
||||
<button class="btn btn-sm" @click="query.page = Math.min(totalPage, query.page + 1)" :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 >= totalPage">Next <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg></button>
|
||||
</div>
|
||||
<span class="admin-filter-inline">
|
||||
<label>Per page</label>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<div class="anonymize-page h-100">
|
||||
<div class="anonymize-page h-100" :class="{'mobile-preview-active': mobilePane === 'preview'}">
|
||||
<!-- ===== STATE 1: No URL — centered input ===== -->
|
||||
<div class="anonymize-landing" v-show="!(sourceUrl)">
|
||||
<div class="anonymize-landing" v-show="!workspaceOpen && !isUpdate">
|
||||
<div class="anonymize-landing-inner">
|
||||
<div class="paper-crumbs">My work / <span class="here">New anonymization</span></div>
|
||||
<h1 class="paper-page-title">New <em>anonymization</em></h1>
|
||||
<p class="paper-page-lede">
|
||||
Paste a GitHub repository, pull-request, or gist URL. We’ll fetch
|
||||
it, strip every trace of identity, and hand you back a stable link.
|
||||
it, redact the terms you choose in text files, and give you a share link.
|
||||
Images and PDFs are served unchanged when enabled. Review them before sharing.
|
||||
</p>
|
||||
<div class="form-group mt-4 mb-2">
|
||||
<div class="source-url-label-row">
|
||||
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
|
||||
<aside class="repo-access" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate && detectedType !== 'gist'">
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><i class="fas fa-shield-alt" aria-hidden="true"></i> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'oauth'" :disabled="githubConnection === 'oauth'" @click="chooseGitHubConnection('oauth')">Legacy OAuth</button>
|
||||
</div>
|
||||
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ detectedType !== 'gist' && githubConnection === 'github-app' ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||||
@@ -21,6 +22,8 @@
|
||||
</div>
|
||||
<input id="sourceUrl-landing" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }">
|
||||
</div>
|
||||
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
||||
<p class="text-danger" role="alert" v-if="sourceUrlError">{{ sourceUrlError }}</p>
|
||||
<small class="form-text" style="color: var(--ink-muted);">
|
||||
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
|
||||
</small>
|
||||
@@ -33,17 +36,17 @@
|
||||
<aside class="repo-access-details" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-main">
|
||||
<div class="repo-access-controls">
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <i class="fas fa-external-link-alt" aria-hidden="true"></i></button>
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><i class="fas fa-info-circle" aria-hidden="true"></i> Selected content will be published, including content from private repositories.</span></div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Selected content will be published, including content from private repositories.</span></div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== -->
|
||||
<div class="anonymize-workspace" v-show="sourceUrl">
|
||||
<div class="anonymize-workspace" v-show="workspaceOpen || isUpdate">
|
||||
<header class="anonymize-topbar">
|
||||
<div class="anonymize-topbar-inner">
|
||||
<div class="paper-crumbs">
|
||||
@@ -62,9 +65,14 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="anonymize-mobile-tabs" role="tablist" aria-label="Anonymization workspace">
|
||||
<button id="workspace-settings-tab" role="tab" type="button" :aria-selected="mobilePane === 'settings'" aria-controls="workspace-settings" :tabindex="mobilePane === 'settings' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('settings')">Settings</button>
|
||||
<button id="workspace-preview-tab" role="tab" type="button" :aria-selected="mobilePane === 'preview'" aria-controls="workspace-preview" :tabindex="mobilePane === 'preview' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('preview')">Reviewer preview</button>
|
||||
</nav>
|
||||
<div class="anonymize-preview-empty" id="workspace-preview" v-if="detectedType !== 'repo' && !details" role="status">{{ sourceLoading ? 'Loading source…' : 'Choose an accessible source to see its preview.' }}</div>
|
||||
<div class="anonymize-split">
|
||||
<!-- Form column (left) -->
|
||||
<div class="anonymize-form-col overflow-auto">
|
||||
<div id="workspace-settings" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-settings-tab' : null" class="anonymize-form-col overflow-auto">
|
||||
<form class="form needs-validation paper-settings-main" name="anonymize" novalidate v-form="viewState">
|
||||
|
||||
<section class="paper-settings-section">
|
||||
@@ -75,7 +83,7 @@
|
||||
<label class="paper-field-label" for="sourceUrl">GitHub URL</label>
|
||||
<aside class="repo-access" aria-label="GitHub access" v-if="sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate && detectedType !== 'gist'">
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><i class="fas fa-shield-alt" aria-hidden="true"></i> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> <span class="sr-only">Read-only </span>GitHub App</button>
|
||||
<button type="button" :aria-pressed="githubConnection === 'oauth'" :disabled="githubConnection === 'oauth'" @click="chooseGitHubConnection('oauth')">Legacy OAuth</button>
|
||||
</div>
|
||||
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ detectedType !== 'gist' && githubConnection === 'github-app' ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||||
@@ -99,11 +107,11 @@
|
||||
<aside class="repo-access-details" aria-label="GitHub access" v-if="sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
||||
<div class="repo-access-main">
|
||||
<div class="repo-access-controls">
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <i class="fas fa-external-link-alt" aria-hidden="true"></i></button>
|
||||
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||||
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><i class="fas fa-info-circle" aria-hidden="true"></i> Selected content will be published, including content from private repositories.</span></div>
|
||||
<div class="repo-access-note"><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Select repositories on GitHub, then return here.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Selected content will be published, including content from private repositories.</span></div>
|
||||
</aside>
|
||||
|
||||
<aside class="repo-access-details" v-if="isUpdate && detectedType === 'repo' && role === 'owner' && sourceUrl === 'https://github.com/' + _originalFullName">
|
||||
@@ -120,8 +128,8 @@
|
||||
<option v-for="(b, index) in branches" v-text="b?.name" :value="(b?.name)"></option>
|
||||
</select>
|
||||
<div class="input-group-append">
|
||||
<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>
|
||||
<button class="btn btn-icon" type="button" title="Refresh" data-toggle="tooltip" data-placement="bottom" @click="getBranches(true)" aria-label="Refresh">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,10 +215,12 @@
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value => { options.image = value }, value: options?.image, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="image">Display images</label>
|
||||
<small class="form-text text-muted">Images are served unchanged and may contain names or affiliations.</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value => { options.pdf = value }, value: options?.pdf, form: "anonymize", options: {} }">
|
||||
<label class="form-check-label" for="pdf">Display PDFs</label>
|
||||
<small class="form-text text-muted">PDF contents and metadata are served unchanged. <a href="/faq#formats">About supported formats</a></small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value => { options.notebook = value }, value: options?.notebook, form: "anonymize", options: {} }">
|
||||
@@ -219,10 +229,10 @@
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value => { options.page = value }, value: options?.page, form: "anonymize", options: {} }" :disabled="!details?.hasPage || (details?.pageSource && details?.pageSource?.branch !== source?.branch)">
|
||||
<label class="form-check-label" for="page">GitHub Pages</label>
|
||||
<small class="form-text text-muted d-block" v-show="!details?.hasPage">
|
||||
<small class="form-text text-muted" v-if="!details?.hasPage">
|
||||
{{ fmt?.translate('WARNINGS.page_not_enabled_on_repo') }}
|
||||
</small>
|
||||
<small class="form-text text-muted d-block" v-show="details?.hasPage && details?.pageSource && details?.pageSource?.branch !== source?.branch">
|
||||
<small class="form-text text-muted" v-if="details?.hasPage && details?.pageSource?.branch && source?.branch && details.pageSource.branch !== source.branch">
|
||||
{{ fmt?.translate('WARNINGS.page_branch_mismatch', { pageBranch: details?.pageSource?.branch, selectedBranch: source?.branch }) }}
|
||||
</small>
|
||||
</div>
|
||||
@@ -335,8 +345,8 @@
|
||||
<img alt="" style="width: 24px; height: 24px; border-radius: 50%;" v-if="c?.photo" :src="safeUrl((c?.photo))">
|
||||
<a target="_blank" v-text="c?.username" :href="safeUrl("https://github.com/" + (c?.username))"></a>
|
||||
<span class="type-badge type-coauthor">Co-author</span>
|
||||
<button type="button" class="btn btn-sm" style="margin-left: auto;" title="Remove co-author" v-show="role === 'owner' || role === 'admin'" @click="removeCoauthor(c)">
|
||||
<i class="fas fa-times"></i>
|
||||
<button type="button" class="btn btn-sm btn-icon" style="margin-left: auto;" title="Remove co-author" v-show="role === 'owner' || role === 'admin'" @click="removeCoauthor(c)" aria-label="Remove co-author">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text text-muted" v-show="!coauthors || coauthors?.length === 0">
|
||||
@@ -347,44 +357,61 @@
|
||||
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
|
||||
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
||||
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved in this tab for 30 minutes." : "Unsaved changes. Keep this tab open until you save." }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved settings" : "Discard changes" }}</button></div>
|
||||
<p class="anonymize-review-note" v-if="detectedType === 'repo' && (options.image || options.pdf)">Review enabled images and PDFs before sharing. Their contents are not anonymized.</p>
|
||||
<button type="button" class="btn btn-outline-ink mobile-preview-jump" @click="selectMobilePane('preview')">Review preview <span aria-hidden="true">→</span></button>
|
||||
<div class="anonymize-submit-bar" v-show="detectedType">
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'repo' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Repository
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'repo' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Repository
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'repo' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Repository
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'repo' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeRepo($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Repository
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Pull Request
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Pull Request
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Pull Request
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Pull Request
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize Gist
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Gist
|
||||
</button>
|
||||
<button type="submit" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update Gist
|
||||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Gist
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Preview column (right) -->
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'repo' && html_readme">
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'repo'">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">README with redactions applied</span>
|
||||
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'README with redactions applied' : 'README preview' }}</span>
|
||||
</div>
|
||||
<div class="anonymize-preview-body markdown-body body" v-html="sanitize(html_readme)"></div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-status" v-if="!sourceLoading && !readme && previewStatus === 'idle'">No README is available to preview. Review the published files before sharing.</div>
|
||||
<div class="anonymize-preview-body markdown-body body" v-if="previewStatus === 'ready'" v-html="sanitize(html_readme)"></div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'gist' && details">
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'gist' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">Gist with redactions applied</span>
|
||||
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'Gist with redactions applied' : 'Gist preview' }}</span>
|
||||
</div>
|
||||
<div class="anonymize-preview-body">
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-body" v-if="previewStatus === 'ready'">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizeGistContent(details?.gist?.description) || 'Untitled gist' }}</span>
|
||||
@@ -413,7 +440,7 @@
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.gist?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<i class="far fa-user"></i> @<span v-text="anonymizeGistContent(comment?.author)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizeGistContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
@@ -426,12 +453,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anonymize-preview-col" v-if="detectedType === 'pr' && details">
|
||||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'pr' && details">
|
||||
<div class="anonymize-preview-head">
|
||||
<span class="paper-eyebrow">Live preview</span>
|
||||
<span class="anonymize-preview-sub">Pull request with redactions applied</span>
|
||||
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'Pull request with redactions applied' : 'Pull request preview' }}</span>
|
||||
</div>
|
||||
<div class="anonymize-preview-body">
|
||||
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
||||
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
||||
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
||||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||||
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
||||
</div>
|
||||
<div class="anonymize-preview-body" v-if="previewStatus === 'ready'">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
|
||||
<h2 class="pr-title mb-1">
|
||||
<span v-if="options?.title">{{ anonymizePrContent(details?.pullRequest?.title) }}</span>
|
||||
@@ -446,24 +479,25 @@
|
||||
<markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
|
||||
</div>
|
||||
<nav class="paper-tabs" role="tablist" v-if="options?.diff || options?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{'active': prTabState?.active == 'diff'}" @click="prTabState.active = 'diff'">
|
||||
<i class="fas fa-code"></i> Diff
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{'active': prTabState?.active == 'diff'}" id="preview-pr-diff-tab" :aria-selected="prTabState?.active === 'diff'" aria-controls="preview-pr-diff-panel" :tabindex="prTabState?.active === 'diff' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'diff'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Diff
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{'active': prTabState?.active == 'comments'}" @click="prTabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{'active': prTabState?.active == 'comments'}" id="preview-pr-comments-tab" :aria-selected="prTabState?.active === 'comments'" aria-controls="preview-pr-comments-panel" :tabindex="prTabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="anonymize-preview-status" v-if="!options?.diff && !options?.comments">Diff and comments are hidden by your display settings.</div>
|
||||
<div class="paper-tab-content">
|
||||
<div v-if="options?.diff && prTabState?.active == 'diff'">
|
||||
<div role="tabpanel" id="preview-pr-diff-panel" aria-labelledby="preview-pr-diff-tab" v-if="options?.diff && prTabState?.active == 'diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(anonymizePrContent(details?.pullRequest?.diff)))"></div>
|
||||
</div>
|
||||
<div v-if="options?.comments && prTabState?.active == 'comments'">
|
||||
<div role="tabpanel" id="preview-pr-comments-panel" aria-labelledby="preview-pr-comments-tab" v-if="options?.comments && prTabState?.active == 'comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.pullRequest?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="options?.username">
|
||||
<i class="far fa-user"></i> @<span v-text="anonymizePrContent(comment?.author)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizePrContent(comment?.author)"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<div class="paper-section-eyebrow anonymize-section-title">
|
||||
<i class="fas fa-chalkboard-teacher"></i> Conference ID
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conference ID
|
||||
</div>
|
||||
<!-- Conference -->
|
||||
<div class="form-group">
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
|
||||
<div class="paper-section-eyebrow anonymize-section-title">
|
||||
<i class="fas fa-shield-alt"></i> Anonymization Options
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymization Options
|
||||
</div>
|
||||
<!-- Pull Request ID -->
|
||||
<div class="form-group">
|
||||
@@ -112,7 +112,7 @@
|
||||
<div class="card-header" id="headingOne">
|
||||
<h2 class="mb-0">
|
||||
<button 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
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-filter"></use></svg> Advanced options
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -169,10 +169,10 @@
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
<div class="anonymize-submit-bar" v-show="pullRequestUrl">
|
||||
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="!isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||||
<i class="fas fa-user-secret mr-1"></i> Anonymize
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize
|
||||
</button>
|
||||
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="isUpdate" @click.prevent="submitForm($event, () => { updatePullRequest($event) })">
|
||||
<i class="fas fa-save mr-1"></i> Update
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page claim-page">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">Claim</span></div>
|
||||
<h1 class="paper-page-title">Claim an <em>anonymization</em></h1>
|
||||
<p class="paper-page-lede">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container page paper-page">
|
||||
<div class="container page paper-page conference-page">
|
||||
<div>
|
||||
<div class="paper-crumbs">
|
||||
<a href="/dashboard">My work</a> / <a href="/conferences">Conferences</a> / <span class="here">{{ conference?.conferenceID }}</span>
|
||||
@@ -15,7 +15,7 @@
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + conference?.status"></span>
|
||||
<span v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</span>
|
||||
<a class="btn btn-outline-ink" :href="safeUrl("/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("/conference/" + (conference?.conferenceID) + "/edit")"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit conference</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-meta-rule">
|
||||
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
<div class="paper-table-row" role="row" v-for="(repo, index) in filteredRepositories" :key="repo?.repoId" :class="{'repo-inactive': repo?.status == 'expired' || repo?.status == 'removed', 'repo-error': repo?.status == 'error'}">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Repo</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Repository"><use href="#project-type-repo"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="repo?.repoId" :href="safeUrl("/r/" + (repo?.repoId) + "/")"></a>
|
||||
<div class="anon-sub">
|
||||
@@ -116,8 +116,8 @@
|
||||
</div>
|
||||
<div class="status-sub" v-if="repo?.anonymizeDate">anonymized {{ fmt?.humanTime(repo?.anonymizeDate) }}</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(repo?.pageView)"></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<div class="cell-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> <span v-text="fmt?.number(repo?.pageView)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span class="expires-never" v-if="repo?.status == 'ready' && (repo?.options?.expirationMode === 'never' || !repo?.options?.expirationDate)">Never</span>
|
||||
<span v-if="repo?.status == 'ready' && repo?.options?.expirationMode !== 'never' && repo?.options?.expirationDate" v-text="fmt?.humanTime(repo?.options?.expirationDate)"></span>
|
||||
<span class="expires-past" v-if="repo?.status == 'expired'">Expired<span v-if="repo?.options?.expirationDate"> {{ fmt?.humanTime(repo?.options?.expirationDate) }}</span></span>
|
||||
@@ -126,21 +126,21 @@
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<button 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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/r/" + (repo?.repoId) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/anonymize/" + (repo?.repoId))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredRepositories?.length == 0">
|
||||
<i class="fas fa-inbox" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span v-if="!conference?.repositories?.length">No repository has been submitted to this conference yet.</span>
|
||||
<span v-if="conference?.repositories?.length">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions" v-if="conference?.repositories?.length">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container page paper-page">
|
||||
<div class="container page paper-page conferences-page">
|
||||
<div class="row">
|
||||
<div class="w-100">
|
||||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">Conferences</span></div>
|
||||
@@ -8,7 +8,7 @@
|
||||
<p class="paper-page-lede">Group anonymizations by venue, give chairs a shared dashboard, and set one expiry for every submission.</p>
|
||||
</div>
|
||||
<a href="/conference/new" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> New conference
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New conference
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conference-feedback" role="alert" v-if="error"><span>{{ error }}</span><button type="button" class="btn btn-outline-ink" @click="loadConferences()">Retry</button></div>
|
||||
<div class="conference-feedback" role="status" v-if="loading">Loading conferences…</div>
|
||||
<div class="paper-table paper-table-conferences w-100" role="table" aria-label="Conferences">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader">Conference</div>
|
||||
@@ -80,7 +82,7 @@
|
||||
</div>
|
||||
<div class="paper-table-row row-clickable" role="row" v-for="(conference, index) in filteredConferences" :key="conference?.conferenceID" :class="{'repo-inactive': conference?.status == 'expired' || conference?.status == 'removed'}">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge type-repo">Conf</span>
|
||||
<svg class="ui-icon record-type-icon" role="img" aria-label="Conference"><use href="#project-icon-conference"></use></svg>
|
||||
<div class="anon-text">
|
||||
<a class="repo-name" v-text="conference?.name" :href="safeUrl("/conference/" + (conference?.conferenceID))"></a>
|
||||
<div class="anon-sub">
|
||||
@@ -94,40 +96,40 @@
|
||||
<span class="status-word" v-text="fmt?.statusLabel(conference?.status)"></span>
|
||||
</div>
|
||||
</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-views num" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> <span v-text="fmt?.number(conference?.nbRepositories || 0)"></span></div>
|
||||
<div class="cell-expires" role="cell"><svg class="ui-icon mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span v-if="conference?.startDate">{{ fmt?.date(conference?.startDate, 'mediumDate') }} – {{ fmt?.date(conference?.endDate, 'mediumDate') }}</span>
|
||||
<span class="empty-dash" aria-label="No review window" v-if="!conference?.startDate">—</span>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<div class="dropdown">
|
||||
<button 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>
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :disabled="conference?.removing" :aria-label=""Actions for " + (conference?.name)">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/conference/" + (conference?.conferenceID) + "/edit")">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit
|
||||
</a>
|
||||
<div v-if="conference?.status != 'removed'">
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" @click="removeConference(conference)">
|
||||
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paper-table-empty" v-if="filteredConferences?.length == 0">
|
||||
<i class="fas fa-inbox" aria-hidden="true"></i>
|
||||
<div class="paper-table-empty" v-if="!loading && !error && filteredConferences?.length == 0">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span v-if="conferences?.length == 0">You have not created a conference yet.</span>
|
||||
<span v-if="conferences?.length > 0">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions">
|
||||
<button type="button" class="btn btn-outline-ink" v-if="conferences?.length > 0" @click="search = ''; filters.status.ready = true; filters.status.expired = true; filters.status.removed = true;">Clear filters</button>
|
||||
<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>
|
||||
<a href="/conference/new" class="btn btn-ink" v-if="conferences?.length == 0"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New conference</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,17 +15,17 @@
|
||||
<p>Read-only access to the repositories you choose.</p>
|
||||
<p class="paper-inline-alert paper-inline-alert-error" v-if="connections.appError" role="alert">{{ connections.appError }}. Reconnect to restore access.</p>
|
||||
<div class="connection-actions">
|
||||
<a class="btn btn-ink" v-if="connections.appConnected && !connections.appError" target="_self" href="/github/app/install">Manage repository access <i class="fas fa-external-link-alt" aria-hidden="true"></i></a>
|
||||
<a class="btn btn-ink" v-if="connections.appConnected && !connections.appError" target="_self" href="/github/app/install">Manage repository access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||||
<a :class="connections.appConnected && !connections.appError ? 'connection-link' : 'btn btn-ink'" target="_self" href="/github/app/login">{{ connections.appConnected ? 'Reconnect account' : 'Connect GitHub account' }}</a>
|
||||
</div>
|
||||
<p class="connection-help">Choose repositories on GitHub. Organization access may need an administrator's approval.</p>
|
||||
<ul class="connection-installations" v-if="connections.installations?.length">
|
||||
<li v-for="installation in connections.installations" :key="installation.id"><div><strong>{{ installation.account }}</strong><span v-if="installation.suspended"> · Suspended</span></div><a target="_self" :href="'/github/app/install?installationId=' + installation.id">Manage access <span class="sr-only">for {{ installation.account }}</span> →</a></li>
|
||||
</ul>
|
||||
<button class="connection-link" :disabled="busy" @click="loadConnections()"><i class="fas fa-sync-alt" aria-hidden="true"></i> Refresh access</button>
|
||||
<button class="connection-link" :disabled="busy" @click="loadConnections()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Refresh access</button>
|
||||
</section>
|
||||
<section>
|
||||
<div class="connection-top"><i class="fas fa-key" aria-hidden="true"></i><span class="connection-badge connection-badge-muted">Legacy</span></div>
|
||||
<div class="connection-top"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-key"></use></svg><span class="connection-badge connection-badge-muted">Legacy</span></div>
|
||||
<div class="connection-title"><h2>GitHub OAuth</h2><span class="connection-status" :class="{'is-connected': connections.oauthConnected}">{{ connections.oauthConnected ? 'Connected' : 'Not connected' }}</span></div>
|
||||
<p>For existing OAuth anonymizations and gists.</p>
|
||||
<p class="connection-help">This connection uses GitHub's broader repository permissions. Existing work keeps using it until you switch below.</p>
|
||||
@@ -40,7 +40,7 @@
|
||||
<section class="connection-resources">
|
||||
<div class="connection-title"><h2>Repository connections</h2><span class="connection-count">{{ connections.resources?.length || 0 }} resources</span></div>
|
||||
<p>Check access, then switch each resource to the GitHub App. After reinstalling the App, reconnect existing resources here to keep their anonymous URLs and published snapshots.</p>
|
||||
<div class="connection-empty" v-if="!connections.resources?.length"><i class="fas fa-code-branch" aria-hidden="true"></i><div><h3>No repository connections yet</h3><p>Your repositories and pull requests will appear here once you anonymize them.</p></div><a class="connection-link" href="/anonymize">New anonymization →</a></div>
|
||||
<div class="connection-empty" v-if="!connections.resources?.length"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg><div><h3>No repository connections yet</h3><p>Your repositories and pull requests will appear here once you anonymize them.</p></div><a class="connection-link" href="/anonymize">New anonymization →</a></div>
|
||||
<div class="connection-resource" v-for="resource in connections.resources" :key="resource.type + resource.id">
|
||||
<div class="connection-resource-name"><strong>{{ resource.name || resource.id }}</strong><small>{{ resource.type }} · {{ resource.id }}</small></div>
|
||||
<span class="connection-badge" :class="{'connection-badge-muted': resource.connection === 'oauth'}">{{ resource.connection === 'github-app' ? 'Read-only App' : 'Legacy OAuth' }}</span>
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
<button v-if="!['archived', 'removed', 'preparing', 'queue', 'removing', 'expiring'].includes(resource.status) && (resource.status !== 'download' || (resource.type === 'pull-request' && (!resource.statusDate || new Date(resource.statusDate).getTime() <= Date.now() - 300000))) && resource.connection === 'github-app' && connections.oauthConnected" class="connection-link" :disabled="busy" @click="changeConnection(resource, 'oauth', false)">Use OAuth</button>
|
||||
</div>
|
||||
<p class="connection-footnote"><i class="fas fa-info-circle" aria-hidden="true"></i> Removing access stops new fetches from GitHub. Published content keeps its current expiration and removal settings.</p>
|
||||
<p class="connection-footnote"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Removing access stops new fetches from GitHub. Published content keeps its current expiration and removal settings.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,64 +1,59 @@
|
||||
<div class="container page dashboard-page paper-page">
|
||||
<div class="row">
|
||||
<div class="w-100">
|
||||
<div class="paper-crumbs">My work / <span class="here">Dashboard</span></div>
|
||||
<div class="d-flex align-items-end justify-content-between flex-wrap" style="gap: 12px;">
|
||||
<div class="dashboard-heading">
|
||||
<div>
|
||||
<h1 class="paper-page-title">Your <em>anonymizations</em></h1>
|
||||
<p class="paper-page-lede">Every repository, pull request, and gist you’ve mirrored, with live status and stats.</p>
|
||||
<h1 class="paper-page-title">Projects</h1>
|
||||
<p class="paper-page-lede">Manage your anonymous links.</p>
|
||||
<details class="dashboard-quota" :open="quotaExpanded" @toggle="quotaExpanded = $event.target.open" @keydown.esc.prevent.stop="closeQuota()">
|
||||
<summary aria-controls="dashboard-usage" :aria-expanded="quotaExpanded">
|
||||
<svg class="quota-summary-ring" viewBox="0 0 24 24" aria-hidden="true" :class="quota ? 'quota-' + quota.repository.level : ''">
|
||||
<circle class="quota-ring-track" cx="12" cy="12" r="9"></circle>
|
||||
<circle class="quota-ring-fill" cx="12" cy="12" r="9" pathLength="100" v-if="quota && !quota.repository.unlimited" :stroke-dasharray="quota.repository.percent + ' 100'"></circle>
|
||||
<path v-if="quota?.repository.unlimited" d="M7 12c0-4 4-4 5 0s5 4 5 0-4-4-5 0-5 4-5 0" class="quota-ring-unlimited"></path>
|
||||
</svg>
|
||||
<span class="quota-summary-label">Usage</span>
|
||||
<span class="quota-summary-value" v-if="quota">{{ fmt.number(quota.repository.used) }}<span v-if="!quota.repository.unlimited"> / {{ fmt.number(quota.repository.total) }}</span> repos</span>
|
||||
<svg class="project-type-svg quota-summary-chevron" aria-hidden="true"><use href="#project-icon-chevron"></use></svg>
|
||||
</summary>
|
||||
<div class="quota-panel" id="dashboard-usage">
|
||||
<div class="quota-panel-heading"><h2>Account usage</h2><button type="button" class="project-close" aria-label="Close usage" @click="closeQuota()"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></div>
|
||||
<quota-usage :quota="quota" :fmt="fmt" v-if="quota"></quota-usage>
|
||||
<p class="quota-loading" role="status" v-else>Loading usage…</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="dashboard-heading-actions">
|
||||
<a href="/anonymize" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> New anonymization
|
||||
<a href="/anonymize" class="btn btn-ink" aria-label="New anonymization">
|
||||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-plus"></use></svg><span class="dashboard-new-label">New link</span><span class="dashboard-new-mobile" aria-hidden="true">New</span>
|
||||
</a>
|
||||
<a href="/connections" class="dashboard-connections-link">
|
||||
<i class="fab fa-github" aria-hidden="true"></i> GitHub connections <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quota -->
|
||||
<div class="quota-row" v-if="quota">
|
||||
<div class="quota-item" v-for="(q, index) in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label">{{ q?.label }}</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'count'">
|
||||
{{ 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" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'bytes'">
|
||||
{{ 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" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="quota-track" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q?.key].level" :aria-label="(q?.label) + " quota"" :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 ? 'unlimited' : fmt.number(quota[q?.key].percent, 0) + '% used')">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + filters -->
|
||||
<form class="w-100 dashboard-filter-row" aria-label="Filter anonymizations" accept-charset="UTF-8" v-form="viewState" @submit.prevent="submitForm($event, () => { $event.preventDefault() })">
|
||||
<div class="search-wrap">
|
||||
<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 => { search = value }, value: search, form: null, options: {} }">
|
||||
<input type="search" id="search" class="form-control" aria-label="Search anonymizations" placeholder="Search projects, IDs, or conferences…" autocomplete="off" v-field="{ state: viewState, set: value => { search = value }, value: search, form: null, options: {} }">
|
||||
</div>
|
||||
<div class="dashboard-filter-controls">
|
||||
<button class="btn dashboard-filters-toggle" type="button" :aria-expanded="filtersExpanded" aria-controls="dashboard-filter-controls" @click="filtersExpanded = !filtersExpanded">Filters<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-filter"></use></svg></button>
|
||||
<div id="dashboard-filter-controls" class="dashboard-filter-controls" :class="{'expanded': filtersExpanded}">
|
||||
<div class="btn-group" role="group" aria-label="Type">
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'all'}" @click="typeFilter = 'all'" :aria-pressed="(typeFilter === 'all')">All</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'repo'}" @click="typeFilter = 'repo'" :aria-pressed="(typeFilter === 'repo')">Repos</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'pr'}" @click="typeFilter = 'pr'" :aria-pressed="(typeFilter === 'pr')">PRs</button>
|
||||
<button type="button" class="btn" :class="{'btn-primary': typeFilter === 'gist'}" @click="typeFilter = 'gist'" :aria-pressed="(typeFilter === 'gist')">Gists</button>
|
||||
<button type="button" class="btn dashboard-type-filter" title="Repositories" :class="{'btn-primary': typeFilter === 'repo'}" @click="typeFilter = 'repo'" :aria-pressed="(typeFilter === 'repo')"><svg class="project-type-svg" aria-hidden="true"><use href="#project-type-repo"></use></svg><span class="type-filter-label">Repos</span></button>
|
||||
<button type="button" class="btn dashboard-type-filter" title="Pull requests" :class="{'btn-primary': typeFilter === 'pr'}" @click="typeFilter = 'pr'" :aria-pressed="(typeFilter === 'pr')"><svg class="project-type-svg" aria-hidden="true"><use href="#project-type-pr"></use></svg><span class="type-filter-label">PRs</span></button>
|
||||
<button type="button" class="btn dashboard-type-filter" title="Gists" :class="{'btn-primary': typeFilter === 'gist'}" @click="typeFilter = 'gist'" :aria-pressed="(typeFilter === 'gist')"><svg class="project-type-svg" aria-hidden="true"><use href="#project-type-gist"></use></svg><span class="type-filter-label">Gists</span></button>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dashboard-extra-filters" class="dashboard-extra-filters" :class="{'expanded': filtersExpanded}">
|
||||
<div class="dropdown">
|
||||
<button 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-value">{{ sortLabel() }}</span>
|
||||
<i class="fas" aria-hidden="true" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
<svg class="project-type-svg sort-direction" aria-hidden="true" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
<span class="sr-only">{{ sortDesc() ? 'descending' : 'ascending' }}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownSort">
|
||||
@@ -69,7 +64,6 @@
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item" @click="toggleSortDirection()">
|
||||
<i class="fas fa-exchange-alt fa-rotate-90" aria-hidden="true"></i>
|
||||
{{ sortDesc() ? 'Switch to ascending' : 'Switch to descending' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -90,43 +84,48 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Result count + active filter chips -->
|
||||
<div class="dashboard-meta" v-if="!loading">
|
||||
<div class="dashboard-view-tabs" role="group" aria-label="Project view">
|
||||
<button type="button" :aria-pressed="projectView === 'all'" @click="setProjectView('all')" aria-label="All projects"><span class="dashboard-view-long">All projects</span><span class="dashboard-view-short" aria-hidden="true">All</span></button>
|
||||
<button type="button" :aria-pressed="projectView === 'attention'" @click="setProjectView('attention')" :aria-label="'Needs attention ' + attentionCount()"><span class="dashboard-attention-prefix">Needs </span><span>attention</span> <span class="attention-count" :class="{'has-attention': attentionCount() > 0}">{{ attentionCount() }}</span></button>
|
||||
</div>
|
||||
<span class="dashboard-count">
|
||||
<span v-if="filteredItems?.length === items?.length">{{ fmt?.number(items?.length) }} anonymization<span v-if="items?.length !== 1">s</span></span>
|
||||
<span v-if="filteredItems?.length === items?.length">{{ fmt?.number(items?.length) }} project<span v-if="items?.length !== 1">s</span></span>
|
||||
<span v-if="filteredItems?.length !== items?.length">{{ fmt?.number(filteredItems?.length) }} of {{ fmt?.number(items?.length) }} shown</span>
|
||||
</span>
|
||||
<template v-for="(v, f, index) in filters?.status"><span class="filter-chip" v-if="v === false">
|
||||
Hiding {{ statusKeyLabels[f] }}
|
||||
<button type="button" class="filter-chip-close" @click="filters.status[f] = true;" :aria-label=""Show " + (statusKeyLabels[f]) + " again"">×</button>
|
||||
</span></template>
|
||||
<button type="button" class="btn-link-inline" v-if="hasActiveFilters()" @click="clearFilters()">Clear filters</button>
|
||||
<div class="dashboard-active-filters">
|
||||
<template v-for="(v, f, index) in filters?.status"><button type="button" class="filter-chip" v-if="v === false" @click="filters.status[f] = true" :aria-label="'Show ' + statusKeyLabels[f] + ' again'" :title="'Show ' + statusKeyLabels[f] + ' again'">{{ statusKeyLabels[f] }} hidden<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></template>
|
||||
<button type="button" class="filter-chip" v-if="typeFilter !== 'all'" @click="typeFilter = 'all'" aria-label="Show all project types">{{ typeFilter === 'repo' ? 'Repositories' : typeFilter === 'pr' ? 'Pull requests' : 'Gists' }}<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button>
|
||||
<button type="button" class="btn-link-inline" v-if="showClearFilters()" @click="clearFilters()">Clear filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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" role="table" aria-label="Anonymizations" :aria-busy="(loading)">
|
||||
<div class="paper-table-head" role="row">
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('_name') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('_name')}" @click="setSort('_name')">
|
||||
Anonymization <i class="fas" aria-hidden="true" v-if="isSortedBy('_name')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('_label') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('_label')}" @click="setSort('_label')">
|
||||
Project <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('_label')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('status') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('status')}" @click="setSort('status')">
|
||||
Status <i class="fas" aria-hidden="true" v-if="isSortedBy('status')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
Status <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('status')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" class="num" :aria-sort="(isSortedBy('pageView') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('pageView')}" @click="setSort('pageView')">
|
||||
Views <i class="fas" aria-hidden="true" v-if="isSortedBy('pageView')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
Views <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('pageView')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader" :aria-sort="(isSortedBy('options.expirationDate') ? (sortDesc() ? 'descending' : 'ascending') : 'none')">
|
||||
<button type="button" class="sortable" :class="{active: isSortedBy('options.expirationDate')}" @click="setSort('options.expirationDate')">
|
||||
Expires <i class="fas" aria-hidden="true" v-if="isSortedBy('options.expirationDate')" :class="sortDesc() ? 'fa-arrow-down' : 'fa-arrow-up'"></i>
|
||||
Expires <svg class="project-type-svg sort-direction" aria-hidden="true" v-if="isSortedBy('options.expirationDate')" :class="{'ascending': !sortDesc()}"><use href="#project-icon-arrow"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div role="columnheader"><span class="sr-only">Actions</span></div>
|
||||
@@ -143,27 +142,21 @@
|
||||
|
||||
<template v-for="(item, index) in filteredItems" :key="(item?._type + ':' + (item?._id || index))"><div class="paper-table-row" role="row" v-if="!loading" :class="{'repo-inactive': item?._statusKey == 'expired' || item?._statusKey == 'removed', 'repo-error': item?.status == 'error', 'row-clickable': !!item?._viewUrl}" @click="openItem(item, $event)">
|
||||
<div class="cell-anon" role="cell">
|
||||
<span class="type-badge" :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="project-type-icon" role="img" :aria-label="item?._type === 'repo' ? 'Repository' : item?._type === 'pr' ? 'Pull request' : 'Gist'" :title="item?._type === 'repo' ? 'Repository' : item?._type === 'pr' ? 'Pull request' : 'Gist'"><svg class="project-type-svg" aria-hidden="true"><use :href="'#project-type-' + item._type"></use></svg></span>
|
||||
<div class="anon-text">
|
||||
<div class="anon-title">
|
||||
<a class="repo-name" v-if="item?._viewUrl" v-text="item?._name" :href="safeUrl((item?._viewUrl))"></a>
|
||||
<span class="repo-name repo-name-static" v-if="!item?._viewUrl" v-text="item?._name"></span>
|
||||
<a class="repo-name" v-if="item?._viewUrl" :title="item._label" v-text="item?._label" :href="safeUrl((item?._viewUrl))"></a>
|
||||
<span class="repo-name repo-name-static" v-if="!item?._viewUrl" v-text="item?._label"></span>
|
||||
<span class="type-badge type-coauthor" title="You are a co-author on this anonymization" v-if="item?.role === 'coauthor'">Co-author</span>
|
||||
</div>
|
||||
<div class="anon-sub" v-if="!item?._broken || item?.conference">
|
||||
<span class="anon-source" v-if="item?._type === 'repo' && item?.source?.fullName && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?.source?.fullName" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/")"></a><span v-if="item?.options?.update && item?.source?.branch"> · <a target="_blank" rel="noopener" v-text="item?.source?.branch" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/tree/" + (item?.source?.branch))"></a></span><span v-if="item?.source?.commit"> · <a class="commit-hash" target="_blank" rel="noopener" :href="safeUrl("https://github.com/" + (item?.source?.fullName) + "/tree/" + (item?.source?.commit))">@{{ item?.source?.commit?.substring(0, 8) }}</a></span>
|
||||
</span>
|
||||
<span class="anon-source" v-if="item?._type === 'pr' && item?.source?.repositoryFullName && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl("https://github.com/" + (item?.source?.repositoryFullName) + "/pull/" + (item?.source?.pullRequestId))"></a>
|
||||
</span>
|
||||
<span class="anon-source" v-if="item?._type === 'gist' && item?.source?.gistId && !item?._broken">
|
||||
<a target="_blank" rel="noopener" v-text="item?._source" :href="safeUrl("https://gist.github.com/" + (item?.source?.gistId))"></a>
|
||||
</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 class="anon-sub">
|
||||
<code class="anonymous-id" v-if="item?._id" :title="item._id">{{ item._id }}</code>
|
||||
<span class="commit-hash" v-if="item?._type === 'repo' && item?.source?.commit" :title="item.source.commit">{{ item.source.commit.substring(0, 8) }}</span>
|
||||
<span class="cell-conf conf-tag" v-if="item?.conference" :title="(item?.conference)"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-conference"></use></svg> <span v-text="item?.conference"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-row-meta" role="presentation">
|
||||
<div class="cell-status" role="cell">
|
||||
<div class="status-line">
|
||||
<span class="status-dot" aria-hidden="true" :class="'status-' + item?._statusKey"></span>
|
||||
@@ -173,60 +166,91 @@
|
||||
<div class="status-sub status-sub-warn" v-if="item?._stale" :title=""Last activity " + (fmt?.humanTime(item?.anonymizeDate || item?.lastView))">
|
||||
Last activity {{ fmt?.humanTime(item?.anonymizeDate || item?.lastView) }} · may be stuck
|
||||
</div>
|
||||
<div class="status-sub" v-if="item?.status != 'error' && !item?._stale && item?.anonymizeDate">
|
||||
<a class="project-attention-action" v-if="item.status === 'error' && item._editUrl" :href="safeUrl(item._editUrl)">Fix source</a>
|
||||
<button type="button" class="project-attention-action" v-if="item._stale || item._broken" @click.stop="showProjectDetails(item)">{{ item._broken ? 'Review details' : 'Check progress' }}</button>
|
||||
<div class="status-sub status-sub-date" v-if="item?.status != 'error' && !item?._stale && item?.anonymizeDate">
|
||||
anonymized {{ fmt?.humanTime(item?.anonymizeDate) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" v-text="fmt?.number(item?.pageView)"></div>
|
||||
<div class="cell-views num" role="cell" :aria-label="fmt.number(item.pageView) + ' views'"><svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg><span>{{ fmt.number(item.pageView) }}</span></div>
|
||||
<div class="cell-expires" role="cell">
|
||||
<svg class="project-type-svg mobile-meta-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg>
|
||||
<span class="expires-never" v-if="item?._expiry?.kind === 'never'">Never</span>
|
||||
<span v-if="item?._expiry?.kind === 'date'" v-text="fmt?.humanTime(item?._expiry?.date)"></span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && item?._expiry?.date">Expired {{ fmt?.humanTime(item?._expiry?.date) }}</span>
|
||||
<span class="expires-past" v-if="item?._expiry?.kind === 'expired' && !item?._expiry?.date">Expired</span>
|
||||
<span class="empty-dash" aria-label="Not applicable" v-if="item?._expiry?.kind === 'none'">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell-actions" role="cell">
|
||||
<button type="button" class="copy-link-inline" v-if="item._viewUrl" @click.stop="copyLink(item)" :disabled="!!copyingId" :title="'Copy anonymous link for ' + item._label" :aria-label="'Copy anonymous link for ' + item._label"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-copy"></use></svg></button>
|
||||
<button type="button" class="project-details-button" v-if="item._broken" @click.stop="showProjectDetails(item)" :data-project-actions="encodeURIComponent(item._type + ':' + item._id)" :aria-controls="projectDetailsId(item)" :aria-expanded="selectedProject === item">Details</button>
|
||||
<div class="dropdown" v-if="!item?._broken">
|
||||
<button 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>
|
||||
<button class="btn btn-icon-dots" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :data-project-actions="encodeURIComponent(item._type + ':' + item._id)" :aria-label=""Actions for " + (item?._name)">
|
||||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-more"></use></svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<button type="button" class="dropdown-item" @click="showProjectDetails(item)" :aria-controls="projectDetailsId(item)" :aria-expanded="selectedProject === item">Project details & name</button>
|
||||
<a class="dropdown-item" :href="safeUrl((item?._viewUrl))">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View
|
||||
View
|
||||
</a>
|
||||
<a class="dropdown-item" target="_self" v-if="item?._type === 'repo' && item?.options?.page && item?.status == 'ready'" :href="safeUrl("/w/" + (item?.repoId) + "/")">
|
||||
<i class="fas fa-globe" aria-hidden="true"></i> View page
|
||||
View page
|
||||
</a>
|
||||
<button type="button" class="dropdown-item" @click="copyLink(item)" :disabled="!!copyingId" v-if="item?._viewUrl">
|
||||
Copy anonymous link
|
||||
</button>
|
||||
<a class="dropdown-item" :href="safeUrl((item?._editUrl))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
Edit
|
||||
</a>
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'ready' || item?.status == 'error'" @click="refreshItem(item)">
|
||||
<i class="fas fa-sync" aria-hidden="true"></i> Force update
|
||||
Force update
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'removed'" @click="refreshItem(item)">
|
||||
<i class="fas fa-check-circle" aria-hidden="true"></i> Enable
|
||||
Enable
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" v-if="item?.status == 'expired'" @click="extendItem(item)">
|
||||
<i class="fas fa-calendar-plus" aria-hidden="true"></i> Extend 6 months
|
||||
Extend 6 months
|
||||
</button>
|
||||
<div v-if="(item?.status == 'ready' || item?.status == 'expired' || item?.status == 'error') && item?.role !== 'coauthor'">
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item dropdown-item-danger" @click="removeItem(item)">
|
||||
<i class="fas fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-details-row" role="row" v-if="!loading && selectedProject === item">
|
||||
<div class="project-details" role="cell" aria-colspan="5" :id="projectDetailsId(item)" tabindex="-1" @keydown.esc.stop="closeProjectDetails(item)">
|
||||
<div class="project-details-heading"><h2>Project details</h2><button type="button" class="project-close" @click="closeProjectDetails(item)" aria-label="Close project details"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-close"></use></svg></button></div>
|
||||
<dl class="project-details-list">
|
||||
<div><dt>Source</dt><dd>{{ item._source || 'Not available' }}</dd></div>
|
||||
<div><dt>Anonymous ID</dt><dd><code>{{ item._id || 'Missing identifier' }}</code></dd></div>
|
||||
<div v-if="item.source?.commit"><dt>Commit</dt><dd><code>{{ item.source.commit }}</code></dd></div>
|
||||
<div v-if="item.conference"><dt>Conference</dt><dd>{{ item.conference }}</dd></div>
|
||||
<div><dt>Status</dt><dd>{{ fmt.statusLabel(item.status) }}<span v-if="item.statusMessage"> · {{ fmt.statusMsg(item.statusMessage) }}</span></dd></div>
|
||||
<div v-if="item.anonymizeDate"><dt>Last anonymized</dt><dd>{{ fmt.humanTime(item.anonymizeDate) }}</dd></div>
|
||||
</dl>
|
||||
<p class="project-progress-help" v-if="item._stale">This download has not progressed recently. Check the source access and selected branch before retrying.</p>
|
||||
<a class="project-attention-action" v-if="item._editUrl" :href="safeUrl(item._editUrl)">{{ needsAttention(item) ? 'Check source settings' : 'Edit anonymization settings' }}</a>
|
||||
<form class="project-name-form" v-if="item._id" @submit.prevent="saveProjectName(item)">
|
||||
<label :for="'project-name-' + item._type + '-' + item._id">Project name</label>
|
||||
<p :id="'project-name-help-' + item._type + '-' + item._id">Only you see this name. Leave it blank to use the default.</p>
|
||||
<div class="project-name-controls"><input class="form-control" type="text" :id="'project-name-' + item._type + '-' + item._id" :aria-describedby="'project-name-help-' + item._type + '-' + item._id" maxlength="100" v-model="projectName" :disabled="nameSaving"><button class="btn btn-ink" type="submit" :disabled="nameSaving">{{ nameSaving ? 'Saving…' : 'Save name' }}</button></div>
|
||||
<p class="project-name-error" role="alert" v-if="nameError">{{ nameError }}</p><p role="status" v-if="nameMessage">{{ nameMessage }}</p>
|
||||
</form>
|
||||
</div>
|
||||
</div></template>
|
||||
|
||||
<div class="paper-table-empty" v-if="!loading && filteredItems?.length == 0">
|
||||
<i class="fas fa-inbox" aria-hidden="true"></i>
|
||||
<svg class="project-type-svg project-empty-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span v-if="items?.length == 0">You have no anonymizations yet.</span>
|
||||
<span v-if="items?.length > 0">Nothing matches the current filters.</span>
|
||||
<div class="paper-table-empty-actions">
|
||||
<button type="button" class="btn btn-outline-ink" v-if="hasActiveFilters()" @click="clearFilters()">Clear filters</button>
|
||||
<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>
|
||||
<a href="/anonymize" class="btn btn-ink" v-if="items?.length == 0"><svg class="project-type-svg" aria-hidden="true"><use href="#project-icon-plus"></use></svg> New anonymization</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="explorer-page">
|
||||
<button class="sidebar-toggle" v-show="files?.length" @click="sidebarCollapsed = !sidebarCollapsed" :aria-label="(sidebarCollapsed ? 'Show files' : 'Hide files')">
|
||||
<i class="fas" :class="sidebarCollapsed ? 'fa-folder-open' : 'fa-times'"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="sidebarCollapsed ? '#ui-icon-folder' : '#project-icon-close'"></use></svg>
|
||||
<span v-text="sidebarCollapsed ? 'Files' : 'Close'"></span>
|
||||
</button>
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<div class="leftCol-head">
|
||||
<span class="leftCol-eyebrow">Files</span>
|
||||
<button class="leftCol-close" aria-label="Close files" @click="sidebarCollapsed = true">
|
||||
<i class="fas fa-times"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="leftCol-search">
|
||||
<div class="tree-search-box">
|
||||
<i class="fas tree-search-icon" :class="fileSearchLoading ? 'fa-spinner fa-spin' : 'fa-search'"></i>
|
||||
<svg class="ui-icon tree-search-icon" aria-hidden="true" :class="{'fa-spin': fileSearchLoading}"><use :href="fileSearchLoading ? '#ui-icon-spinner' : '#ui-icon-search'"></use></svg>
|
||||
<input type="text" class="tree-search-input" placeholder="Search files" aria-label="Search files" v-field="{ state: viewState, set: value => { fileSearchQuery = value }, value: fileSearchQuery, form: null, options: { debounce: 300 }, change: () => { onFileSearchChange() } }">
|
||||
<kbd class="tree-search-kbd" v-show="!(fileSearchQuery)">{{ isMac ? '⌘' : 'Ctrl+' }}K</kbd>
|
||||
<button class="tree-search-clear" aria-label="Clear search" v-show="fileSearchQuery" @click="fileSearchQuery = ''; onFileSearchChange()">×</button>
|
||||
@@ -25,11 +25,11 @@
|
||||
</div>
|
||||
<div class="leftCol-body">
|
||||
<div class="paper-inline-warning" role="alert" v-if="options?.truncatedFolders?.length > 0">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
{{ fmt?.translate('WARNINGS.repo_truncated') }}
|
||||
</div>
|
||||
<div class="paper-inline-warning" role="alert" v-if="options?.hasSubmodules">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
{{ fmt?.translate('WARNINGS.submodules_not_included') }}
|
||||
</div>
|
||||
<tree class="files" :file="files" :search-query="fileSearchQuery" :search-results="fileSearchResults" :page="viewState"></tree>
|
||||
@@ -58,19 +58,19 @@
|
||||
<li class="breadcrumb-item" v-for="(p, index) in paths" v-text="p"></li>
|
||||
</ol>
|
||||
<div class="status-bar-actions">
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/anonymize/" + (repoId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/anonymize/" + (repoId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg><span class="d-none d-md-inline"> Edit</span></a>
|
||||
<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 == 'html-doc' && !showSource" :class="{'btn-active': allowScripts}" @click="toggleAllowScripts()">
|
||||
<i class="fab fa-js"></i><span class="d-none d-md-inline">
|
||||
{{ allowScripts ? "JS on" : "JS off" }}</span>
|
||||
</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 == 'html-doc'" @click="toggleSource()">
|
||||
<i class="fas" :class="showSource ? 'fa-eye' : 'fa-code'"></i><span class="d-none d-md-inline">
|
||||
<svg class="ui-icon" aria-hidden="true"><use :href="showSource ? '#project-icon-eye' : '#ui-icon-code'"></use></svg><span class="d-none d-md-inline">
|
||||
{{ showSource ? "Rendered" : "Source" }}</span>
|
||||
</button>
|
||||
<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>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Download current file" title="Download the current file" v-show="content != null" :href="safeUrl((url) + "&download=true")"><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a>
|
||||
<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("/api/repo/" + (repoId) + "/zip")"><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> Full repo ZIP</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Website" v-if="options?.hasWebsite" :href="safeUrl("/w/" + (repoId) + "/")"><i class="fas fa-globe"></i><span class="d-none d-md-inline"> Website</span></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))"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-file"></use></svg><span class="d-none d-md-inline"> Raw</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Download current file" title="Download the current file" v-show="content != null" :href="safeUrl((url) + "&download=true")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-download"></use></svg><span class="d-none d-md-inline"> Download</span></a>
|
||||
<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("/api/repo/" + (repoId) + "/zip")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-archive"></use></svg><span class="d-none d-md-inline"> Full repo ZIP</span></a>
|
||||
<a target="_self" class="btn btn-sm" aria-label="Website" v-if="options?.hasWebsite" :href="safeUrl("/w/" + (repoId) + "/")"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-globe"></use></svg><span class="d-none d-md-inline"> Website</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="explorer-content">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- General -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-info-circle mr-2"></i>General
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-info"></use></svg>General
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-general" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
<!-- Features -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-cogs mr-2"></i>Features
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#project-icon-filter"></use></svg>Features
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-features" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -285,7 +285,7 @@ anonymous_github</code></pre>
|
||||
|
||||
<!-- Limitations -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>Limitations
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>Limitations
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-limitations" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -322,7 +322,7 @@ anonymous_github</code></pre>
|
||||
|
||||
<!-- Privacy & Security -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-shield-alt mr-2"></i>Privacy & Security
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#ui-icon-shield"></use></svg>Privacy & Security
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-privacy" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
@@ -355,28 +355,9 @@ anonymous_github</code></pre>
|
||||
</div>
|
||||
<div id="permissions" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingPermissions">
|
||||
<div class="panel-body p-3">
|
||||
<p>
|
||||
Anonymous GitHub only reads your repositories — it
|
||||
never pushes commits, opens issues, modifies settings, or
|
||||
deletes anything. From your perspective as a user, the
|
||||
service is read-only.
|
||||
</p>
|
||||
<p>
|
||||
However, GitHub's OAuth scopes do not offer a read-only
|
||||
option for private repositories: the only scope that grants
|
||||
access to private repos is <code>repo</code>, which is
|
||||
documented as full read/write access. To support users who
|
||||
want to anonymize a private repository, Anonymous GitHub
|
||||
must request that scope, and GitHub then displays the
|
||||
broader permission text at sign-in. The application itself
|
||||
only ever performs read operations against the GitHub API.
|
||||
</p>
|
||||
<p>
|
||||
If you only anonymize public repositories, the source code
|
||||
is open and can be audited on the
|
||||
<a href="https://github.com/tdurieux/anonymous_github/" target="_blank">GitHub repository</a>,
|
||||
or you can self-host your own instance.
|
||||
</p>
|
||||
<p>The GitHub App has read-only access to the repositories you choose. You can change repository access on your <a href="/connections">GitHub connections</a> page.</p>
|
||||
<p>Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection.</p>
|
||||
<p>Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,7 +384,7 @@ anonymous_github</code></pre>
|
||||
|
||||
<!-- Self-Hosting -->
|
||||
<h4 class="faq-cat">
|
||||
<i class="fas fa-server mr-2"></i>Self-Hosting
|
||||
<svg class="ui-icon mr-2" aria-hidden="true"><use href="#project-icon-storage"></use></svg>Self-Hosting
|
||||
</h4>
|
||||
<div class="panel-group" id="faq-hosting" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default mb-3">
|
||||
|
||||
@@ -6,40 +6,42 @@
|
||||
</div>
|
||||
|
||||
<header class="pr-header">
|
||||
<div class="pr-title-row">
|
||||
<h1 class="paper-page-title pr-title">
|
||||
<span v-if="details?.description" v-text="details?.description"></span>
|
||||
<span class="text-muted" v-if="!details?.description">Untitled gist</span>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/gist-anonymize/" + (gistId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</h1>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/gist-anonymize/" + (gistId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</div>
|
||||
<div class="pr-header-meta">
|
||||
<span class="paper-pill" :class="{'good': details?.isPublic, 'warn': !details?.isPublic}">
|
||||
{{ details?.isPublic ? 'Public' : 'Secret' }}
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.ownerLogin">
|
||||
<i class="far fa-user"></i> @<span v-text="details?.ownerLogin"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="details?.ownerLogin"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.updatedDate">
|
||||
<i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.anonymizeDate">
|
||||
<i class="fas fa-user-secret"></i> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="paper-tabs" role="tablist" v-if="(details?.files && details?.files?.length) || (details?.comments && details?.comments?.length)">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.files" :class="{'active': tabState?.active == 'files'}" @click="tabState.active = 'files'">
|
||||
<i class="fas fa-file-code"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.files" :class="{'active': tabState?.active == 'files'}" id="gist-files-tab" :aria-selected="tabState?.active === 'files'" aria-controls="gist-files-panel" :tabindex="tabState?.active === 'files' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'files'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-type-gist"></use></svg>
|
||||
<span v-text="fmt.plural(details?.files?.length, {'0': 'No files', 'one': '1 file', 'other': '{} files'})"></span>
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" @click="tabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" id="gist-comments-tab" :aria-selected="tabState?.active === 'comments'" aria-controls="gist-comments-panel" :tabindex="tabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="paper-tab-content">
|
||||
<div v-if="details?.files && tabState?.active =='files'">
|
||||
<div role="tabpanel" id="gist-files-panel" aria-labelledby="gist-files-tab" v-if="details?.files && tabState?.active =='files'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(file, index) in details?.files">
|
||||
<div class="pr-comment-head">
|
||||
@@ -49,18 +51,18 @@
|
||||
<gist-file :file="file"></gist-file>
|
||||
</li>
|
||||
<li class="paper-table-empty" v-if="!details?.files?.length">
|
||||
<i class="fas fa-file"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-file"></use></svg>
|
||||
<span>No files in this gist.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="details?.comments && tabState?.active =='comments'">
|
||||
<div role="tabpanel" id="gist-comments-panel" aria-labelledby="gist-comments-tab" v-if="details?.comments && tabState?.active =='comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="comment?.author">
|
||||
<i class="far fa-user"></i> @<span v-text="comment?.author"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="comment?.author"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
@@ -69,7 +71,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="paper-table-empty" v-if="!details?.comments?.length">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span>No comments on this gist.</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<nav class="navbar navbar-expand-lg" :class="{'navbar-dark': isDarkMode}">
|
||||
<a class="navbar-brand" href="/">Anonymous <em>GitHub</em></a>
|
||||
<a class="nav-support" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener" title="Donate on Ko-fi to help cover hosting costs" aria-label="Donate on Ko-fi to help cover hosting costs">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>
|
||||
<span class="nav-support-label">Support</span>
|
||||
</a>
|
||||
<button 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>
|
||||
</button>
|
||||
@@ -13,28 +17,28 @@
|
||||
</li>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/dashboard" :class="{'active': path == '/dashboard'}">
|
||||
<i class="fas fa-th-large d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-grid"></use></svg>
|
||||
My work
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/anonymize" :class="{'active':path == '/anonymize' || path == '/pull-request-anonymize'}">
|
||||
<i class="fas fa-user-secret d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg>
|
||||
Anonymize
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" v-if="user">
|
||||
<a class="nav-link" href="/conferences" :class="{'active':path == '/conferences'}">
|
||||
<i class="fas fa-chalkboard-teacher d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#project-icon-conference"></use></svg>
|
||||
Conferences
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/faq" :class="{'active':path == '/faq'}"><i class="fas fa-question-circle d-lg-none mr-1"></i>FAQ</a>
|
||||
<a class="nav-link" href="/faq" :class="{'active':path == '/faq'}"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-help"></use></svg>FAQ</a>
|
||||
</li>
|
||||
<li class="nav-item" v-if="user && user?.isAdmin">
|
||||
<a class="nav-link" href="/admin/" :class="{'active':path?.indexOf('/admin') === 0}">
|
||||
<i class="fas fa-cog d-lg-none mr-1"></i>
|
||||
<svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#project-icon-filter"></use></svg>
|
||||
Admin
|
||||
</a>
|
||||
</li>
|
||||
@@ -47,19 +51,19 @@
|
||||
anonymized repository often think it is about the paper, so the
|
||||
label names the service and the tooltip says who to contact. -->
|
||||
<li class="nav-item d-none d-lg-block">
|
||||
<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
|
||||
<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"><svg class="ui-icon d-lg-none mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>Report a bug
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" v-if="!isDarkMode">
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-moon"></use></svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" v-if="isDarkMode">
|
||||
<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>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-sun"></use></svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -76,7 +80,7 @@
|
||||
<div class="dropdown-menu dropdown-menu-right user-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
<h6 class="dropdown-header">Signed in as @{{ user?.username }}</h6>
|
||||
<a class="dropdown-item" href="/profile">
|
||||
<i class="fas fa-sliders-h" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg>
|
||||
<span>Settings<small>Defaults, quotas, account</small></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="/connections">
|
||||
@@ -84,21 +88,25 @@
|
||||
<span>GitHub connections<small>Repository access and OAuth</small></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="/claim">
|
||||
<i class="fas fa-link" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg>
|
||||
<span>Claim an anonymization<small>Attach an existing mirror to your account</small></span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">
|
||||
<i class="fas fa-bug" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg>
|
||||
<span>Report a bug<small>About this service, not a paper</small></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github" aria-hidden="true"></i>
|
||||
<span>Source code</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg>
|
||||
<span>Support the project</span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="/api/user/logout" target="_self">
|
||||
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-logout"></use></svg>
|
||||
<span>Sign out</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
Anonymous GitHub makes a read-only mirror of your repository, pull
|
||||
request, or gist with every trace of identity removed, so you can
|
||||
attach a link to your submission without breaking anonymity.
|
||||
request, or gist. It redacts the terms you choose in text files,
|
||||
so reviewers can browse your artifact through an anonymous link.
|
||||
</p>
|
||||
|
||||
<div class="paper-cta-row">
|
||||
@@ -34,7 +34,7 @@
|
||||
<span class="dots"><i></i><i></i><i></i></span>
|
||||
<span class="url">anonymous.4open.science/anonymize</span>
|
||||
</div>
|
||||
<img src="/imgs/anonymize.png" width="2880" height="1800" alt="The anonymize form with a live README preview showing the redactions">
|
||||
<img src="/imgs/redaction-preview.png" width="1360" height="960" alt="A reviewer preview with the author, affiliation, and email replaced by anonymous terms">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -117,9 +117,9 @@
|
||||
<h2>Anonymize, review, <em>manage.</em></h2>
|
||||
</div>
|
||||
<p>
|
||||
Every mirror is read-only and passes through the same filter, from the
|
||||
README to a PDF's link targets. Monitor it, update it, or remove it the
|
||||
day the decision is out.
|
||||
Terms are redacted in text files. Enabled images and PDFs are served
|
||||
unchanged, so review them for identifying content before sharing.
|
||||
Monitor your mirror, update it, or remove it when the review is over.
|
||||
</p>
|
||||
</section>
|
||||
<section class="paper-features paper-wrap" aria-label="Product features">
|
||||
@@ -160,19 +160,19 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="q">
|
||||
<a href="/faq#howWork">Why does GitHub ask for write access at sign-in?</a>
|
||||
<a href="/faq#permissions">What access does GitHub sign-in require?</a>
|
||||
<p>
|
||||
GitHub's OAuth scopes have no read-only option for private
|
||||
repositories. Anonymous GitHub only ever reads your repositories; it
|
||||
never pushes, modifies, or deletes anything.
|
||||
The GitHub App has read-only access to the repositories you choose.
|
||||
Legacy OAuth uses broader permissions and is still needed for gists.
|
||||
Anonymous GitHub never writes to your repositories.
|
||||
</p>
|
||||
</div>
|
||||
<div class="q">
|
||||
<a href="/faq#formats">Which file formats are supported?</a>
|
||||
<p>
|
||||
Source code with syntax highlighting, Markdown, Jupyter notebooks,
|
||||
PDFs, images, and GitHub Pages sites. Everything passes through the
|
||||
same redaction filter.
|
||||
PDFs, images, and GitHub Pages sites. Text files are anonymized;
|
||||
enabled images and PDFs are served unchanged.
|
||||
</p>
|
||||
</div>
|
||||
<div class="q">
|
||||
@@ -191,7 +191,7 @@
|
||||
<div class="paper-footer-mark">Anonymous <em>GitHub</em></div>
|
||||
<p class="paper-footer-tag">
|
||||
A read-only, identity-stripped mirror of your repository, built for
|
||||
double-blind peer review.
|
||||
double-anonymous peer review.
|
||||
</p>
|
||||
</div>
|
||||
<div class="paper-footer-col">
|
||||
@@ -215,7 +215,7 @@
|
||||
</div>
|
||||
<div class="paper-footer-rule"></div>
|
||||
<div class="paper-footer-foot">
|
||||
<span>© {{ year || 2026 }} Anonymous GitHub · MIT licensed</span>
|
||||
<span>© {{ year || 2026 }} Anonymous GitHub · GPL-3.0 licensed</span>
|
||||
<span class="paper-footer-meta">Built for reviewers, by researchers.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<svg class="ui-icon-definitions" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<symbol id="project-type-repo" viewBox="0 0 24 24">
|
||||
<path d="M6 3h13v18H6a3 3 0 0 1 0-6h13M3 18V6a3 3 0 0 1 3-3M8 7h6"></path>
|
||||
</symbol>
|
||||
<symbol id="project-type-pr" viewBox="0 0 24 24">
|
||||
<circle cx="6" cy="5" r="2.5"></circle><circle cx="6" cy="19" r="2.5"></circle><circle cx="18" cy="19" r="2.5"></circle>
|
||||
<path d="M6 7.5v9M18 16.5V9a4 4 0 0 0-4-4h-3m3-3-3 3 3 3"></path>
|
||||
</symbol>
|
||||
<symbol id="project-type-gist" viewBox="0 0 24 24">
|
||||
<path d="M14 3H5v18h14V8l-5-5v5h5M9 12l-2 2 2 2m6-4 2 2-2 2"></path>
|
||||
</symbol>
|
||||
<symbol id="project-icon-copy" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="13" rx="2"></rect><path d="M15 8V3H3v13h5"></path></symbol>
|
||||
<symbol id="project-icon-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle></symbol>
|
||||
<symbol id="project-icon-eye" viewBox="0 0 24 24"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle></symbol>
|
||||
<symbol id="project-icon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></symbol>
|
||||
<symbol id="project-icon-conference" viewBox="0 0 24 24"><path d="m3 8 9-5 9 5H3Zm2 3v7m5-7v7m4-7v7m5-7v7M3 21h18"></path></symbol>
|
||||
<symbol id="project-icon-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></symbol>
|
||||
<symbol id="project-icon-filter" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"></path><circle cx="8" cy="6" r="2"></circle><circle cx="16" cy="12" r="2"></circle><circle cx="9" cy="18" r="2"></circle></symbol>
|
||||
<symbol id="project-icon-arrow" viewBox="0 0 24 24"><path d="M12 4v16m-5-5 5 5 5-5"></path></symbol>
|
||||
<symbol id="project-icon-close" viewBox="0 0 24 24"><path d="m6 6 12 12M6 18 18 6"></path></symbol>
|
||||
<symbol id="project-icon-chevron" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"></path></symbol>
|
||||
<symbol id="project-icon-storage" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="7" rx="2"></rect><rect x="3" y="13" width="18" height="7" rx="2"></rect><path d="M7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6"></path></symbol>
|
||||
<symbol id="project-icon-file" viewBox="0 0 24 24"><path d="M14 3H5v18h14V8l-5-5v5h5M9 12h6M9 16h6"></path></symbol>
|
||||
<symbol id="project-icon-inbox" viewBox="0 0 24 24"><path d="m3 14 3-9h12l3 9v6H3v-6h5l2 3h4l2-3h5"></path></symbol>
|
||||
<symbol id="ui-icon-arrow-left" viewBox="0 0 24 24"><path d="M20 12H4m6-6-6 6 6 6"/></symbol>
|
||||
<symbol id="ui-icon-arrow-right" viewBox="0 0 24 24"><path d="M4 12h16m-6-6 6 6-6 6"/></symbol>
|
||||
<symbol id="ui-icon-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></symbol>
|
||||
<symbol id="ui-icon-check-circle" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m7 12 3 3 7-7"/></symbol>
|
||||
<symbol id="ui-icon-edit" viewBox="0 0 24 24"><path d="m15 4 5 5M4 20l5-1L21 7l-5-5L4 14v6Z"/></symbol>
|
||||
<symbol id="ui-icon-external-link" viewBox="0 0 24 24"><path d="M14 3h7v7m0-7L10 14M10 3H3v18h18v-7"/></symbol>
|
||||
<symbol id="ui-icon-key" viewBox="0 0 24 24"><circle cx="8" cy="8" r="5"/><path d="m12 12 9 9m-6-6 3-3m0 6 3-3"/></symbol>
|
||||
<symbol id="ui-icon-refresh" viewBox="0 0 24 24"><path d="M20 8a8 8 0 0 0-14-3L3 8m0-5v5h5M4 16a8 8 0 0 0 14 3l3-3m0 5v-5h-5"/></symbol>
|
||||
<symbol id="ui-icon-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7h.01"/></symbol>
|
||||
<symbol id="ui-icon-warning" viewBox="0 0 24 24"><path d="m12 3 10 18H2L12 3Zm0 6v5m0 3h.01"/></symbol>
|
||||
<symbol id="ui-icon-help" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9 9a3 3 0 0 1 6 0c0 2-3 2-3 4m0 4h.01"/></symbol>
|
||||
<symbol id="ui-icon-user" viewBox="0 0 24 24"><circle cx="12" cy="7" r="4"/><path d="M4 21v-3a8 8 0 0 1 16 0v3"/></symbol>
|
||||
<symbol id="ui-icon-users" viewBox="0 0 24 24"><circle cx="9" cy="7" r="4"/><path d="M2 21v-3a7 7 0 0 1 14 0v3M16 3a4 4 0 0 1 0 8m3 3a6 6 0 0 1 3 5v2"/></symbol>
|
||||
<symbol id="ui-icon-shield" viewBox="0 0 24 24"><path d="m12 3 9 4v6c0 4-9 9-9 9S3 17 3 13V7l9-4Z"/></symbol>
|
||||
<symbol id="ui-icon-code" viewBox="0 0 24 24"><path d="m7 6-6 6 6 6m10-12 6 6-6 6m-4-14-2 16"/></symbol>
|
||||
<symbol id="ui-icon-comment" viewBox="0 0 24 24"><path d="M21 3H3v14h5l4 4v-4h9V3Z"/><path d="M7 8h10M7 12h6"/></symbol>
|
||||
<symbol id="ui-icon-save" viewBox="0 0 24 24"><path d="M3 3h14l4 4v14H3V3Zm4 0v7h10V3M7 21v-7h10v7"/></symbol>
|
||||
<symbol id="ui-icon-heart" viewBox="0 0 24 24"><path d="M12 21 3 12C-2 6 7-1 12 6c5-7 14 0 9 6l-9 9Z"/></symbol>
|
||||
<symbol id="ui-icon-globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="4" ry="9"/><path d="M3 12h18"/></symbol>
|
||||
<symbol id="ui-icon-hourglass" viewBox="0 0 24 24"><path d="M5 3h14M5 21h14M6 3v4l6 5 6-5V3M6 21v-4l6-5 6 5v4"/></symbol>
|
||||
<symbol id="ui-icon-moon" viewBox="0 0 24 24"><path d="M20 14A9 9 0 0 1 10 3a9 9 0 1 0 10 11Z"/></symbol>
|
||||
<symbol id="ui-icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 1v2m0 18v2M1 12h2m18 0h2M4 4l2 2m12 12 2 2M4 20l2-2M18 6l2-2"/></symbol>
|
||||
<symbol id="ui-icon-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
|
||||
<symbol id="ui-icon-logout" viewBox="0 0 24 24"><path d="M9 3H3v18h6M9 12h12m-4-4 4 4-4 4"/></symbol>
|
||||
<symbol id="ui-icon-link" viewBox="0 0 24 24"><path d="m10 7 3-3a5 5 0 0 1 7 7l-3 3M7 10l-3 3a5 5 0 0 0 7 7l3-3M8 16l8-8"/></symbol>
|
||||
<symbol id="ui-icon-chart" viewBox="0 0 24 24"><path d="M3 3v18h18M7 16V9m5 7V5m5 11v-5"/></symbol>
|
||||
<symbol id="ui-icon-list" viewBox="0 0 24 24"><path d="M9 5h12M9 12h12M9 19h12m-18-14 1 1 2-2m-3 8 1 1 2-2m-3 8 1 1 2-2"/></symbol>
|
||||
<symbol id="ui-icon-bug" viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="14" rx="5"/><path d="M9 7V5a3 3 0 0 1 6 0v2M12 7v14M3 8l4 2m-4 5h4m-4 7 4-4M21 8l-4 2m4 5h-4m4 7-4-4"/></symbol>
|
||||
<symbol id="ui-icon-chevron-left" viewBox="0 0 24 24"><path d="m15 5-7 7 7 7"/></symbol>
|
||||
<symbol id="ui-icon-chevron-right" viewBox="0 0 24 24"><path d="m9 5 7 7-7 7"/></symbol>
|
||||
<symbol id="ui-icon-download" viewBox="0 0 24 24"><path d="M12 3v12m-5-5 5 5 5-5M3 16v5h18v-5"/></symbol>
|
||||
<symbol id="ui-icon-export" viewBox="0 0 24 24"><path d="M13 3H3v18h14v-6M12 9h9m-4-4 4 4-4 4"/></symbol>
|
||||
<symbol id="ui-icon-clipboard" viewBox="0 0 24 24"><path d="M8 5H4v16h16V5h-4"/><rect x="8" y="2" width="8" height="6" rx="2"/></symbol>
|
||||
<symbol id="ui-icon-ban" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m6 6 12 12"/></symbol>
|
||||
<symbol id="ui-icon-trash" viewBox="0 0 24 24"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></symbol>
|
||||
<symbol id="ui-icon-broom" viewBox="0 0 24 24"><path d="m16 3-5 8m-7 5 6-6 6 4-3 7H3l1-5Zm1 5 4-6"/></symbol>
|
||||
<symbol id="ui-icon-database" viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 4 18 4 18 0V5M3 12c0 4 18 4 18 0"/></symbol>
|
||||
<symbol id="ui-icon-user-minus" viewBox="0 0 24 24"><circle cx="9" cy="7" r="4"/><path d="M2 21v-3a7 7 0 0 1 14 0v3m0-11h7"/></symbol>
|
||||
<symbol id="ui-icon-user-shield" viewBox="0 0 24 24"><circle cx="8" cy="6" r="4"/><path d="M1 21v-4a7 7 0 0 1 10-6m7 0 5 2v4c0 3-5 6-5 6s-5-3-5-6v-4l5-2Z"/></symbol>
|
||||
<symbol id="ui-icon-spinner" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-9-9"/></symbol>
|
||||
<symbol id="ui-icon-archive" viewBox="0 0 24 24"><path d="M3 3h18v5H3V3Zm2 5v13h14V8M10 3v3m4 3v3m-4 3v3m4 0v3"/></symbol>
|
||||
<symbol id="ui-icon-calendar" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M7 2v6m10-6v6M3 11h18"/></symbol>
|
||||
<symbol id="ui-icon-search" viewBox="0 0 24 24"><circle cx="10" cy="10" r="7"></circle><path d="m15 15 7 7"></path></symbol>
|
||||
<symbol id="ui-icon-folder" viewBox="0 0 24 24"><path d="M3 20V4h6l2 3h10v13H3Z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -37,7 +37,7 @@
|
||||
<div class="form-group">
|
||||
<label class="paper-field-label" for="url">Website</label>
|
||||
<input type="url" class="form-control" name="url" id="url" placeholder="https://example.org" v-field="{ state: viewState, set: value => { options.url = value }, value: options?.url, form: "conference", options: {} }" :class="{'is-invalid': conference?.url?.invalid && (conference?.url?.touched || conference?.submitted)}">
|
||||
<div class="invalid-feedback" v-show="conference?.url?.errors?.invalid">
|
||||
<div class="invalid-feedback" v-show="conference?.url?.errors?.url">
|
||||
The url of the conference is invalid.
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,8 +189,8 @@
|
||||
<div class="paper-settings-footer">
|
||||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||||
<div class="alert alert-success" role="alert" v-if="message" v-text="message"></div>
|
||||
<button id="send" type="submit" class="btn btn-ink" @click.prevent="submitForm($event, () => { submit($event) })">
|
||||
{{ editionMode ? "Update" : "Create" }} conference
|
||||
<button id="send" :disabled="saving" type="submit" class="btn btn-ink" @click.prevent="submitForm($event, () => { submit($event) })">
|
||||
{{ saving ? (editionMode ? "Updating…" : "Creating…") : (editionMode ? "Update conference" : "Create conference") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="file-error container d-flex h-100" v-if="type == 'rate_limited'">
|
||||
<div class="paper-ratelimit-card m-auto" style="max-width:520px;">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-hourglass"></use></svg>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Temporarily paused</div>
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
</div>
|
||||
<div class="d-flex flex-wrap" style="gap: 6px">
|
||||
<a href="/anonymize" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> Anonymize Repo
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> Anonymize Repo
|
||||
</a>
|
||||
<a href="/pull-request-anonymize" class="btn btn-ink">
|
||||
<i class="fa fa-plus-circle mr-1" aria-hidden="true"></i> Anonymize PR
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-plus"></use></svg> Anonymize PR
|
||||
</a>
|
||||
<a title="Claim the ownership of an existing anonymized repository." data-toggle="tooltip" data-placement="bottom" href="/claim" class="btn">
|
||||
Claim
|
||||
@@ -113,19 +113,19 @@
|
||||
</div>
|
||||
<div class="repo-meta">
|
||||
<span title="Conference" v-if="pr?.conference">
|
||||
<i class="fas fa-chalkboard-teacher"></i> {{ pr?.conference }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> {{ pr?.conference }}
|
||||
</span>
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Terms: " + (pr?.options?.terms?.join(', '))">
|
||||
<i class="fas fa-shield-alt"></i> {{ fmt?.number(pr?.options?.terms?.length) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> {{ fmt?.number(pr?.options?.terms?.length) }}
|
||||
</span>
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Views: " + (fmt?.number(pr?.pageView))">
|
||||
<i class="far fa-eye"></i> {{ fmt?.number(pr?.pageView) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> {{ fmt?.number(pr?.pageView) }}
|
||||
</span>
|
||||
<span data-toggle="tooltip" data-placement="bottom" :title=""Last view: " + (fmt?.date(pr?.lastView))">
|
||||
<i class="far fa-calendar-alt"></i> {{ fmt?.humanTime(pr?.lastView) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-calendar"></use></svg> {{ fmt?.humanTime(pr?.lastView) }}
|
||||
</span>
|
||||
<span v-if="pr?.options?.expirationMode!='never' && pr?.status == 'ready'">
|
||||
<i class="far fa-clock"></i> Expire: {{ fmt?.humanTime(pr?.options?.expirationDate) }}
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> Expire: {{ fmt?.humanTime(pr?.options?.expirationDate) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,26 +136,26 @@
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" :href="safeUrl("/pull-request-anonymize/" + (pr?.pullRequestId))">
|
||||
<i class="far fa-edit" aria-hidden="true"></i> Edit
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'ready' || pr?.status == 'error'" @click.prevent="updatePullRequest(pr)">
|
||||
<i class="fas fa-sync"></i> Force update
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Force update
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'removed'" @click.prevent="updatePullRequest(pr)">
|
||||
<i class="fas fa-check-circle"></i> Enable
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg> Enable
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" v-show="pr?.status == 'ready'" @click.prevent="removePullRequest(pr)">
|
||||
<i class="fas fa-trash-alt"></i> Remove
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove
|
||||
</a>
|
||||
<a class="dropdown-item" :href="safeUrl("/pr/" + (pr?.pullRequestId) + "/")">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i> View PR
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-eye"></use></svg> View PR
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="repo-list-empty" v-if="filteredPullRequests?.length == 0">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-inbox"></use></svg>
|
||||
<span>No pull requests to display.</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -17,32 +17,11 @@
|
||||
</aside>
|
||||
|
||||
<div class="paper-settings-main">
|
||||
<!-- Quota: same markup as the dashboard, driven by quotaService -->
|
||||
<!-- Account usage shares the dashboard component. -->
|
||||
<section id="settings-quota" class="paper-settings-section">
|
||||
<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>
|
||||
<div class="quota-row" v-if="quota">
|
||||
<div class="quota-item" v-for="(q, index) in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes'},
|
||||
{key: 'file', label: 'Files', kind: 'count'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label">{{ q?.label }}</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'count'">
|
||||
{{ 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" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
<span class="quota-value" v-if="q?.kind === 'bytes'">
|
||||
{{ 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" v-if="quota[q?.key].unlimited">Unlimited</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="quota-track" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q?.key].level" :aria-label="(q?.label) + " quota"" :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 ? 'unlimited' : fmt.number(quota[q?.key].percent, 0) + '% used')">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q?.key].percent + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<quota-usage :quota="quota" :fmt="fmt" v-if="quota"></quota-usage>
|
||||
<div class="quota-row" aria-hidden="true" v-if="!quota">
|
||||
<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>
|
||||
@@ -143,11 +122,13 @@ jane.smith@example.org" v-field="{ state: viewState, set: value => { terms =
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
||||
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved in this tab for 30 minutes." : "Unsaved changes. Keep this tab open until you save." }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved settings" : "Discard changes" }}</button></div>
|
||||
<div class="paper-settings-footer">
|
||||
<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-inline-alert paper-inline-alert-error" role="alert" v-if="error"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> <span v-text="error"></span></div>
|
||||
<div class="paper-settings-actions">
|
||||
<button id="save" type="submit" class="btn btn-ink" :disabled="saving">
|
||||
<i class="fas fa-check mr-1" aria-hidden="true" v-if="message"></i>{{ saving ? 'Saving…' : (message ? 'Saved' : 'Save defaults') }}
|
||||
<svg class="ui-icon mr-1" aria-hidden="true" v-if="message"><use href="#ui-icon-check"></use></svg>{{ saving ? 'Saving…' : (message ? 'Saved' : 'Save defaults') }}
|
||||
</button>
|
||||
<span class="paper-settings-hint">Applies to new anonymizations only. Existing ones keep their settings.</span>
|
||||
</div>
|
||||
@@ -178,9 +159,9 @@ jane.smith@example.org" v-field="{ state: viewState, set: value => { terms =
|
||||
revokes the application's access to your GitHub account, and erases
|
||||
your personal data (username, email, access tokens). This cannot be undone.
|
||||
</p>
|
||||
<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>
|
||||
<div class="paper-inline-alert paper-inline-alert-error" role="alert" v-if="deleteError"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> <span v-text="deleteError"></span></div>
|
||||
<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' }}
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-trash"></use></svg>{{ deletingAccount ? 'Deleting…' : 'Delete my account' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
</div>
|
||||
|
||||
<header class="pr-header">
|
||||
<div class="pr-title-row">
|
||||
<h1 class="paper-page-title pr-title">
|
||||
<span v-if="details?.title" v-text="details?.title"></span>
|
||||
<span class="text-muted" v-if="!details?.title">Untitled pull request</span>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/pull-request-anonymize/" + (pullRequestId))"><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</h1>
|
||||
<a class="btn btn-sm" aria-label="Edit" v-if="options?.isAdmin || options?.isOwner" :href="safeUrl("/pull-request-anonymize/" + (pullRequestId))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-edit"></use></svg><span class="d-none d-md-inline"> Edit</span></a>
|
||||
</div>
|
||||
<div class="pr-header-meta">
|
||||
<span class="paper-pill" :class="{'good': details?.merged, 'warn': details?.state == 'open', 'bad': details?.state == 'closed' && !details?.merged}">
|
||||
<span class="status-dot" :class="{'status-ready': details?.merged, 'status-error': details?.state == 'closed' && !details?.merged}"></span>
|
||||
@@ -20,10 +22,10 @@
|
||||
<i class="fab fa-github"></i> <span v-text="details?.baseRepositoryFullName"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.updatedDate">
|
||||
<i class="far fa-clock"></i> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-clock"></use></svg> <span v-text="fmt?.date(details?.updatedDate)"></span>
|
||||
</span>
|
||||
<span class="pr-meta-item" v-if="details?.anonymizeDate">
|
||||
<i class="fas fa-user-secret"></i> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymized <span v-text="fmt?.date(details?.anonymizeDate)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -34,26 +36,26 @@
|
||||
</section>
|
||||
|
||||
<nav class="paper-tabs" role="tablist" v-if="details?.diff || details?.comments">
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.diff" :class="{'active': tabState?.active == 'diff'}" @click="tabState.active = 'diff'">
|
||||
<i class="fas fa-code"></i> Diff
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.diff" :class="{'active': tabState?.active == 'diff'}" id="pr-diff-tab" :aria-selected="tabState?.active === 'diff'" aria-controls="pr-diff-panel" :tabindex="tabState?.active === 'diff' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'diff'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Diff
|
||||
</button>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" @click="tabState.active = 'comments'">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<button class="paper-tab" type="button" role="tab" v-if="details?.comments" :class="{'active': tabState?.active == 'comments'}" id="pr-comments-tab" :aria-selected="tabState?.active === 'comments'" aria-controls="pr-comments-panel" :tabindex="tabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="tabState.active = 'comments'">
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span v-text="fmt.plural(details?.comments?.length, {'0': 'No comments', 'one': '1 comment', 'other': '{} comments'})"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="paper-tab-content">
|
||||
<div v-if="details?.diff && tabState?.active =='diff'">
|
||||
<div role="tabpanel" id="pr-diff-panel" aria-labelledby="pr-diff-tab" v-if="details?.diff && tabState?.active =='diff'">
|
||||
<div class="pr-diff" v-html="sanitize(fmt?.diff(details?.diff))"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="details?.comments && tabState?.active =='comments'">
|
||||
<div role="tabpanel" id="pr-comments-panel" aria-labelledby="pr-comments-tab" v-if="details?.comments && tabState?.active =='comments'">
|
||||
<ul class="pr-comments">
|
||||
<li class="pr-comment" v-for="(comment, index) in details?.comments">
|
||||
<div class="pr-comment-head">
|
||||
<span class="pr-comment-author" v-if="comment?.author">
|
||||
<i class="far fa-user"></i> @<span v-text="comment?.author"></span>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="comment?.author"></span>
|
||||
</span>
|
||||
<span class="pr-comment-date" v-if="comment?.updatedDate" v-text="fmt?.date(comment?.updatedDate)"></span>
|
||||
</div>
|
||||
@@ -62,7 +64,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="paper-table-empty" v-if="!details?.comments?.length">
|
||||
<i class="far fa-comment-dots"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
|
||||
<span>No comments on this pull request.</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="quota-row quota-usage" v-if="quota">
|
||||
<div class="quota-item" :class="'quota-' + quota[q.key].level" :key="q.key" v-for="q in [
|
||||
{key: 'repository', label: 'Repositories', kind: 'count', icon: 'project-type-repo'},
|
||||
{key: 'storage', label: 'Storage', kind: 'bytes', icon: 'project-icon-storage'},
|
||||
{key: 'file', label: 'Files', kind: 'count', icon: 'project-icon-file'}
|
||||
]">
|
||||
<div class="quota-header">
|
||||
<span class="quota-label"><svg class="project-type-svg" aria-hidden="true"><use :href="'#' + q.icon"></use></svg>{{ q.label }}</span>
|
||||
<span class="quota-percent" v-if="!quota[q.key].unlimited">{{ fmt.number(quota[q.key].percent, 0) }}% used</span>
|
||||
<span class="quota-unlimited-tag" v-if="quota[q.key].unlimited">Unlimited</span>
|
||||
</div>
|
||||
<div class="quota-value"><strong>{{ formatAmount(quota[q.key].used, q.kind) }}</strong><span v-if="!quota[q.key].unlimited"> / {{ formatAmount(quota[q.key].total, q.kind) }}</span></div>
|
||||
<div class="quota-track" v-if="!quota[q.key].unlimited" role="progressbar" aria-valuemin="0" :class="'quota-' + quota[q.key].level" :aria-label="q.label + ' quota'" :aria-valuenow="Math.min(quota[q.key].used, quota[q.key].total)" :aria-valuemax="quota[q.key].total" :aria-valuetext="fmt.number(quota[q.key].percent, 0) + '% used'">
|
||||
<div class="quota-fill" v-if="!quota[q?.key].unlimited" :style="{width: quota[q.key].percent + '%'}"></div>
|
||||
</div>
|
||||
<p class="quota-remaining" v-if="!quota[q.key].unlimited && quota[q.key].used < quota[q.key].total">{{ formatAmount(quota[q.key].total - quota[q.key].used, q.kind) }} {{ q.key === 'repository' ? 'repositories available' : q.key === 'file' ? 'files available' : 'available' }}</p>
|
||||
<p class="quota-remaining quota-limit" v-if="!quota[q.key].unlimited && quota[q.key].used >= quota[q.key].total">{{ quota[q.key].used > quota[q.key].total ? 'Limit exceeded' : 'Limit reached' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container paper-page auth-page">
|
||||
<a class="auth-back" href="/"><i class="fas fa-arrow-left" aria-hidden="true"></i> Anonymous GitHub</a>
|
||||
<a class="auth-back" href="/"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-left"></use></svg> Anonymous GitHub</a>
|
||||
<div class="auth-layout">
|
||||
<header class="auth-intro">
|
||||
<div class="paper-eyebrow">Your research, ready for review</div>
|
||||
@@ -18,16 +18,16 @@
|
||||
<h2>Continue with GitHub</h2>
|
||||
<p>Connect through the GitHub App with read-only repository access.</p>
|
||||
<ul class="auth-permissions">
|
||||
<li><i class="fas fa-check" aria-hidden="true"></i> You choose which repositories to share</li>
|
||||
<li><i class="fas fa-check" aria-hidden="true"></i> No permission to write to your code</li>
|
||||
<li><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg> You choose which repositories to share</li>
|
||||
<li><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg> No permission to write to your code</li>
|
||||
</ul>
|
||||
<a class="btn btn-ink auth-continue" target="_self" href="/github/app/login">Sign in with GitHub <i class="fas fa-arrow-right" aria-hidden="true"></i></a>
|
||||
<a class="btn btn-ink auth-continue" target="_self" href="/github/app/login">Sign in with GitHub <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></a>
|
||||
<small>Your existing anonymizations and connections stay with your account.</small>
|
||||
</section>
|
||||
<section class="auth-legacy" v-if="!site_options?.GITHUB_APP_ENABLED && site_options?.GITHUB_OAUTH_ENABLED && !accountRecovery">
|
||||
<div class="connection-title"><h2>Continue with GitHub</h2></div>
|
||||
<p>Sign in to access your account. GitHub OAuth includes write access to private repositories.</p>
|
||||
<a class="connection-link" target="_self" href="/github/login">Sign in with GitHub <i class="fas fa-arrow-right" aria-hidden="true"></i></a>
|
||||
<a class="connection-link" target="_self" href="/github/login">Sign in with GitHub <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></a>
|
||||
</section>
|
||||
<p role="status" v-if="site_options && !site_options.GITHUB_APP_ENABLED && !site_options.GITHUB_OAUTH_ENABLED">GitHub sign-in is currently unavailable. Please try again later.</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container paper-page">
|
||||
<div class="container paper-page status-page">
|
||||
<div class="paper-crumbs">Anonymization / <span class="here">Status</span></div>
|
||||
<div class="d-flex align-items-end justify-content-between flex-wrap" style="gap: 12px;">
|
||||
<div>
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<div class="paper-ratelimit-card" role="status" v-if="rateLimitResetAt">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-hourglass"></use></svg>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Temporarily paused</div>
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<div class="paper-error-card" role="alert" v-if="repo?.status == 'error' && !rateLimitResetAt">
|
||||
<div class="paper-error-head">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Anonymization failed</div>
|
||||
<div class="paper-error-title">Something went wrong while preparing this repository.</div>
|
||||
@@ -55,9 +55,9 @@
|
||||
<li>If you just signed in, the access token may need a moment to propagate — try again.</li>
|
||||
</ul>
|
||||
<div class="paper-error-actions">
|
||||
<a class="btn btn-ink" :href="safeUrl("/anonymize/" + (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="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 btn-ink" :href="safeUrl("/anonymize/" + (repoId))"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-edit"></use></svg> Edit anonymization</a>
|
||||
<a class="btn" href="/faq"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-help"></use></svg> Read the FAQ</a>
|
||||
<a class="btn" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Report a bug in Anonymous GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,10 +74,10 @@
|
||||
|
||||
<div class="anonymize-submit-bar" v-if="repo?.status == 'ready'">
|
||||
<a class="btn btn-ink" target="_self" :href="safeUrl("/r/" + (repoId) + "/")">
|
||||
<i class="far fa-eye mr-1"></i> Go to anonymized repository
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-eye"></use></svg> Go to anonymized repository
|
||||
</a>
|
||||
<a class="btn" target="_self" v-if="repo?.options?.page" :href="safeUrl("/w/" + (repoId) + "/")">
|
||||
<i class="fas fa-globe mr-1"></i> Go to anonymized GitHub page
|
||||
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-globe"></use></svg> Go to anonymized GitHub page
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -91,19 +91,19 @@
|
||||
|
||||
<div class="paper-support-grid">
|
||||
<a class="paper-support-card" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">
|
||||
<div class="paper-support-eyebrow"><i class="fas fa-code-branch"></i> Contribute</div>
|
||||
<div class="paper-support-eyebrow"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Contribute</div>
|
||||
<p>Collaborate by implementing new features and fixing bugs. Help with new file formats or deployment is welcome.</p>
|
||||
<span class="paper-support-cta">Open on GitHub <i class="fas fa-arrow-right"></i></span>
|
||||
<span class="paper-support-cta">Open on GitHub <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></span>
|
||||
</a>
|
||||
<a class="paper-support-card" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">
|
||||
<div class="paper-support-eyebrow"><i class="far fa-comment-dots"></i> Feedback</div>
|
||||
<div class="paper-support-eyebrow"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg> Feedback</div>
|
||||
<p>Tell us about bugs and missing features in Anonymous GitHub itself. Questions about an anonymized repository go to its authors.</p>
|
||||
<span class="paper-support-cta">File an issue <i class="fas fa-arrow-right"></i></span>
|
||||
<span class="paper-support-cta">File an issue <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></span>
|
||||
</a>
|
||||
<a class="paper-support-card" href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">
|
||||
<div class="paper-support-eyebrow"><i class="fas fa-heart"></i> Sponsor</div>
|
||||
<div class="paper-support-eyebrow"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg> Sponsor</div>
|
||||
<p>Server costs hover around $25 a month. Any contribution helps keep the lights on.</p>
|
||||
<span class="paper-support-cta">GitHub Sponsors <i class="fas fa-arrow-right"></i></span>
|
||||
<span class="paper-support-cta">GitHub Sponsors <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-arrow-right"></use></svg></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { reactive } from "vue";
|
||||
import { reactive, nextTick } from "vue";
|
||||
import { createTimers, createListeners } from "./state.js";
|
||||
import { formDraft } from "./drafts.js";
|
||||
|
||||
function previewTabKeydown(event) {
|
||||
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
||||
const tabs = [...event.currentTarget.closest('[role="tablist"]').querySelectorAll('[role="tab"]')];
|
||||
const index = tabs.indexOf(event.currentTarget);
|
||||
const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1
|
||||
: (index + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
||||
event.preventDefault();
|
||||
tabs[next]?.focus();
|
||||
tabs[next]?.click();
|
||||
}
|
||||
|
||||
// Page setup functions run inside Vue effect scopes.
|
||||
export const mainController = function (state, http, location, timeout) {
|
||||
state.previewTabKeydown = previewTabKeydown;
|
||||
state.title = "Main";
|
||||
state.user = { status: "connection" };
|
||||
state.site_options;
|
||||
@@ -51,8 +64,7 @@ export const mainController = function (state, http, location, timeout) {
|
||||
document.head.append(link);
|
||||
$(`link[href='${darkPrismLink}']`).remove();
|
||||
}
|
||||
// Update Ko-fi floating button to match theme
|
||||
var kofiBtn = document.querySelector(".floatingchat-container-wrap-mo498 .floating-chat-kofi-text-container-wrap");
|
||||
const kofiBtn = document.querySelector("[class*='floatingchat-container-wrap'] [class*='floating-chat-kofi-text-container-wrap']");
|
||||
if (kofiBtn) {
|
||||
kofiBtn.style.backgroundColor = on ? "#FAF9F6" : "#1A1815";
|
||||
kofiBtn.style.color = on ? "#1A1815" : "#FAF9F6";
|
||||
@@ -111,7 +123,23 @@ export const mainController = function (state, http, location, timeout) {
|
||||
};
|
||||
|
||||
export const faqController = function (state, http) {
|
||||
const listen = createListeners();};
|
||||
const listen = createListeners();
|
||||
const timers = createTimers();
|
||||
const revealAnswer = () => {
|
||||
const id = window.location.hash.slice(1);
|
||||
const answer = document.getElementById(id);
|
||||
if (!answer?.classList.contains("panel-collapse")) return;
|
||||
$(answer).collapse("show");
|
||||
const heading = document.getElementById(answer.getAttribute("aria-labelledby"));
|
||||
heading?.setAttribute("tabindex", "-1");
|
||||
heading?.focus({ preventScroll: true });
|
||||
heading?.scrollIntoView?.({ block: "start" });
|
||||
};
|
||||
// Router fragments do not emit hashchange, and answers mount after navigation.
|
||||
state.on("routeUpdate", () => timers.timeout(revealAnswer, 0));
|
||||
listen(window, "hashchange", revealAnswer);
|
||||
timers.timeout(revealAnswer, 0);
|
||||
};
|
||||
|
||||
export const profileController = function (state, http, translate, timeout, quotaService) {
|
||||
state.terms = "";
|
||||
@@ -132,6 +160,8 @@ export const profileController = function (state, http, translate, timeout, quot
|
||||
state.quota = quota;
|
||||
}, console.error);
|
||||
|
||||
const draft = formDraft(state, ["terms", "options"], "profile");
|
||||
state.discardDraft = () => draft.discard();
|
||||
function getDefault() {
|
||||
http.get("/api/user/default").then((res) => {
|
||||
const data = res.data || {};
|
||||
@@ -139,6 +169,7 @@ export const profileController = function (state, http, translate, timeout, quot
|
||||
state.terms = data.terms.join("\n");
|
||||
}
|
||||
state.options = Object.assign({}, state.options, data.options);
|
||||
draft.restore();
|
||||
});
|
||||
}
|
||||
getDefault();
|
||||
@@ -146,6 +177,8 @@ export const profileController = function (state, http, translate, timeout, quot
|
||||
let savedTimer = null;
|
||||
state.saveDefault = ($event) => {
|
||||
if ($event && $event.preventDefault) $event.preventDefault();
|
||||
if (state.saving) return;
|
||||
const submitted = draft.checkpoint();
|
||||
const params = {
|
||||
terms: state.terms
|
||||
.split("\n")
|
||||
@@ -157,9 +190,9 @@ export const profileController = function (state, http, translate, timeout, quot
|
||||
state.error = null;
|
||||
http.post("/api/user/default", params).then(
|
||||
() => {
|
||||
getDefault();
|
||||
draft.accept(submitted);
|
||||
state.saving = false;
|
||||
state.message = "Saved";
|
||||
state.message = state.unsavedChanges ? null : "Saved";
|
||||
if (savedTimer) timeout.cancel(savedTimer);
|
||||
savedTimer = timeout(() => {
|
||||
state.message = null;
|
||||
@@ -168,8 +201,9 @@ export const profileController = function (state, http, translate, timeout, quot
|
||||
(error) => {
|
||||
state.saving = false;
|
||||
const code = error && error.data && error.data.error;
|
||||
translate("ERRORS." + code).then((translation) => {
|
||||
state.error = translation;
|
||||
const key = "ERRORS." + code;
|
||||
translate(key).then((translation) => {
|
||||
state.error = translation && translation !== key ? translation : "Unable to save your defaults. Please try again.";
|
||||
}, () => {
|
||||
state.error = "Unable to save your defaults. Please try again.";
|
||||
});
|
||||
@@ -410,6 +444,88 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
}, 250);
|
||||
|
||||
state.quotaExpanded = false;
|
||||
state.closeQuota = () => {
|
||||
state.quotaExpanded = false;
|
||||
nextTick(() => document.querySelector(".dashboard-quota > summary")?.focus());
|
||||
};
|
||||
createListeners()(document, "click", event => {
|
||||
if (state.quotaExpanded && !event.target.closest?.(".dashboard-quota")) state.quotaExpanded = false;
|
||||
});
|
||||
state.filtersExpanded = false;
|
||||
state.projectView = "all";
|
||||
state.selectedProject = null;
|
||||
state.projectName = "";
|
||||
state.nameSaving = false;
|
||||
state.nameError = "";
|
||||
state.nameMessage = "";
|
||||
state.needsAttention = item => item.status === "error" || item._broken
|
||||
|| (statusKey(item.status) === "progress" && item._stale);
|
||||
state.attentionCount = () => state.items.filter(state.needsAttention).length;
|
||||
state.setProjectView = view => {
|
||||
state.projectView = view;
|
||||
if (view === "attention") {
|
||||
state.filters.status.error = true;
|
||||
state.filters.status.progress = true;
|
||||
}
|
||||
};
|
||||
state.projectDetailsId = item => "project-details-" + encodeURIComponent(item._type + ":" + (item._id || item._source || "unknown"));
|
||||
state.showProjectDetails = async item => {
|
||||
state.selectedProject = item;
|
||||
state.projectName = item.projectName || "";
|
||||
state.nameError = "";
|
||||
state.nameMessage = "";
|
||||
await nextTick();
|
||||
const panel = document.getElementById(state.projectDetailsId(item));
|
||||
panel?.focus();
|
||||
panel?.scrollIntoView?.({ block: "nearest" });
|
||||
};
|
||||
state.closeProjectDetails = item => {
|
||||
state.selectedProject = null;
|
||||
nextTick(() => document.querySelector(`[data-project-actions="${encodeURIComponent(item._type + ":" + item._id)}"]`)?.focus());
|
||||
};
|
||||
state.saveProjectName = async item => {
|
||||
if (state.nameSaving || !item._id) return;
|
||||
const name = state.projectName.trim();
|
||||
if (name.length > 100) { state.nameError = "Use 100 characters or fewer."; return; }
|
||||
state.nameSaving = true;
|
||||
state.nameError = "";
|
||||
state.nameMessage = "";
|
||||
try {
|
||||
const res = await http.post("/api/user/project-name", { type: item._type, id: item._id, name });
|
||||
item.projectName = res.data.name;
|
||||
item._label = item.projectName || item._fallbackLabel;
|
||||
if (state.selectedProject === item) state.nameMessage = name ? "Project name saved." : "Default project name restored.";
|
||||
} catch (_) {
|
||||
if (state.selectedProject === item) state.nameError = "The project name could not be saved. Try again.";
|
||||
} finally { state.nameSaving = false; }
|
||||
};
|
||||
state.copyingId = null;
|
||||
state.copyLink = async (item) => {
|
||||
if (!item._viewUrl || state.copyingId) return;
|
||||
const link = new URL(item._viewUrl, window.location.origin).href;
|
||||
state.copyingId = item._type + ":" + item._id;
|
||||
try {
|
||||
if (window.navigator.clipboard?.writeText) {
|
||||
await window.navigator.clipboard.writeText(link);
|
||||
} else {
|
||||
const field = document.createElement("textarea");
|
||||
field.value = link;
|
||||
field.setAttribute("readonly", "");
|
||||
field.style.position = "fixed";
|
||||
field.style.opacity = "0";
|
||||
const focused = document.activeElement;
|
||||
document.body.appendChild(field);
|
||||
try {
|
||||
field.select();
|
||||
if (!document.execCommand?.("copy")) throw Error("Copy unavailable");
|
||||
} finally { field.remove(); focused?.focus(); }
|
||||
}
|
||||
state.addToast({ title: "Link copied", body: "The anonymous link is ready to share." });
|
||||
} catch {
|
||||
state.addToast({ title: "Copy this anonymous link", body: link });
|
||||
} finally { state.copyingId = null; }
|
||||
};
|
||||
state.items = [];
|
||||
state.search = "";
|
||||
state.loading = true;
|
||||
@@ -458,7 +574,8 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
// `orderBy` is kept as the Angular orderBy expression ("-field" for
|
||||
// descending) so saved preferences stay compatible.
|
||||
const sortFields = {
|
||||
_name: { label: "Name", defaultDesc: false },
|
||||
_name: { label: "Anonymous ID", defaultDesc: false },
|
||||
_label: { label: "Project", defaultDesc: false },
|
||||
anonymizeDate: { label: "Anonymize date", defaultDesc: true },
|
||||
status: { label: "Status", defaultDesc: false },
|
||||
lastView: { label: "Last view", defaultDesc: true },
|
||||
@@ -525,6 +642,8 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
item._source = source;
|
||||
item._broken = !id;
|
||||
item._name = id || source || "(unnamed)";
|
||||
item._fallbackLabel = item._type === "gist" ? "Gist " + (id || source || "without an ID") : source || name || item._name;
|
||||
item._label = item.projectName || item._fallbackLabel;
|
||||
item._editUrl = id ? editUrl : null;
|
||||
item._viewUrl = id ? viewUrl : null;
|
||||
if (item._broken) {
|
||||
@@ -597,7 +716,7 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
pr,
|
||||
pr.pullRequestId,
|
||||
pr.pullRequestId,
|
||||
src.repositoryFullName + "#" + src.pullRequestId,
|
||||
src.repositoryFullName ? src.repositoryFullName + "#" + src.pullRequestId : undefined,
|
||||
"/pull-request-anonymize/" + pr.pullRequestId,
|
||||
"/pr/" + pr.pullRequestId + "/"
|
||||
)
|
||||
@@ -637,11 +756,16 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
state.hasHiddenStatus = () => state.hiddenStatusCount() > 0;
|
||||
|
||||
state.hasActiveFilters = () =>
|
||||
state.projectView !== "all" ||
|
||||
state.typeFilter !== "all" ||
|
||||
state.search.trim().length > 0 ||
|
||||
Object.keys(state.filters.status).some((k) => state.filters.status[k] === false);
|
||||
|
||||
state.showClearFilters = () => state.search.trim().length > 0
|
||||
|| state.typeFilter !== "all" || state.hiddenStatusCount() > 1;
|
||||
|
||||
state.clearFilters = () => {
|
||||
state.projectView = "all";
|
||||
state.typeFilter = "all";
|
||||
state.search = "";
|
||||
Object.keys(state.filters.status).forEach((k) => {
|
||||
@@ -655,6 +779,8 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
if (item._type === "repo" && item.repoId == repoId) {
|
||||
item.status = res.data.status;
|
||||
item.statusMessage = res.data.statusMessage;
|
||||
item._statusKey = statusKey(item.status);
|
||||
if (item._statusKey !== "progress") item._stale = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -777,10 +903,12 @@ export const unifiedDashboardController = function (state, http, location, promi
|
||||
};
|
||||
|
||||
state.itemFilter = (item) => {
|
||||
if (state.projectView === "attention" && !state.needsAttention(item)) return false;
|
||||
if (state.typeFilter !== "all" && item._type !== state.typeFilter) return false;
|
||||
if (state.filters.status[item._statusKey] === false) return false;
|
||||
const needle = state.search.trim().toLowerCase();
|
||||
if (needle.length == 0) return true;
|
||||
if (item._label && String(item._label).toLowerCase().indexOf(needle) > -1) return true;
|
||||
if (item._source && String(item._source).toLowerCase().indexOf(needle) > -1) return true;
|
||||
if (item._id && String(item._id).toLowerCase().indexOf(needle) > -1) return true;
|
||||
if (item.conference && String(item.conference).toLowerCase().indexOf(needle) > -1) return true;
|
||||
@@ -894,6 +1022,56 @@ export const statusController = function (state, http, params) {
|
||||
export const anonymizeController = function (state, http, html, params, location, translate, timeout) {
|
||||
// Unified state
|
||||
state.sourceUrl = "";
|
||||
state.workspaceOpen = false;
|
||||
state.sourceUrlError = "";
|
||||
state.previewStatus = "idle";
|
||||
state.sourceLoading = false;
|
||||
state.mobilePane = "settings";
|
||||
const workspaceMedia = window.matchMedia?.("(max-width: 767px)");
|
||||
state.mobileWorkspace = !!workspaceMedia?.matches;
|
||||
if (workspaceMedia?.addEventListener) createListeners()(workspaceMedia, "change", event => { state.mobileWorkspace = event.matches; });
|
||||
state.selectMobilePane = (pane) => { state.mobilePane = pane; };
|
||||
let sourceGeneration = 0;
|
||||
let sourceDisposed = false;
|
||||
const sourceRequests = new Set();
|
||||
const isCurrentSource = (request, branch = false) => !sourceDisposed
|
||||
&& request.generation === sourceGeneration
|
||||
&& request.url === state.sourceUrl.trim()
|
||||
&& request.connection === state.githubConnection
|
||||
&& (!branch || request.branch === state.source.branch);
|
||||
const beginSourceRequest = () => {
|
||||
const request = { generation: sourceGeneration, url: state.sourceUrl.trim(), connection: state.githubConnection, branch: state.source.branch };
|
||||
sourceRequests.add(request);
|
||||
state.sourceLoading = true;
|
||||
return request;
|
||||
};
|
||||
const finishSourceRequest = (request) => {
|
||||
sourceRequests.delete(request);
|
||||
if (!sourceDisposed) state.sourceLoading = [...sourceRequests].some(request => isCurrentSource(request));
|
||||
};
|
||||
state.on("dispose", () => { sourceDisposed = true; sourceGeneration++; });
|
||||
let selectedSourceKey = null;
|
||||
const sourceKey = (parsed) => parsed.gistId && !parsed.repo
|
||||
? "gist:" + parsed.gistId.toLowerCase()
|
||||
: (parsed.owner + "/" + parsed.repo).toLowerCase() + (parsed.pullRequestId ? "/pull/" + parsed.pullRequestId : "");
|
||||
const draft = formDraft(state, ["sourceUrl", "terms", "repoId", "pullRequestId", "gistId", "source", "options", "conference", "githubConnection"], "anonymize:" + (location.path?.() || ""));
|
||||
state.discardDraft = async () => {
|
||||
draft.discard();
|
||||
state.workspaceOpen = !!state.sourceUrl;
|
||||
state.error = null;
|
||||
if (state.sourceUrl) await state.urlSelected(true);
|
||||
else {
|
||||
sourceGeneration++;
|
||||
state.sourceLoading = false;
|
||||
state.detectedType = null;
|
||||
state.details = null;
|
||||
state.sourceUrlError = "";
|
||||
scheduleReadmePreview.cancel();
|
||||
refreshPrPreview.cancel();
|
||||
refreshGistPreview.cancel();
|
||||
state.previewStatus = "idle";
|
||||
}
|
||||
};
|
||||
state.githubConnection = undefined;
|
||||
state.githubConnections = null;
|
||||
state.gistOAuthRequired = false;
|
||||
@@ -985,6 +1163,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
data.options && data.options.expirationDate
|
||||
? new Date(data.options.expirationDate)
|
||||
: defaultExpirationDate();
|
||||
if (!params.repoId && !params.pullRequestId && !params.gistId && !state.anonymize?.terms?.dirty) state.terms = state.defaultTerms;
|
||||
if (cb) cb();
|
||||
});
|
||||
}
|
||||
@@ -1039,7 +1218,12 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
let saved;
|
||||
try { saved = JSON.parse(sessionStorage.getItem("github-access-draft") || "null"); }
|
||||
catch (_) { return false; }
|
||||
if (!saved || saved.path !== location.path() || Date.now() - saved.savedAt >= 30 * 60000) return false;
|
||||
if (!saved || saved.path !== location.path() || Date.now() - saved.savedAt >= 30 * 60000) {
|
||||
if (!draft.restore()) return false;
|
||||
if (state.sourceUrl) await refreshGitHubAccess();
|
||||
return true;
|
||||
}
|
||||
draft.restore();
|
||||
for (const key of ["sourceUrl", "terms", "repoId", "pullRequestId", "gistId", "source", "options", "conference", "githubConnection"]) {
|
||||
if (Object.prototype.hasOwnProperty.call(saved.draft, key)) state[key] = saved.draft[key];
|
||||
}
|
||||
@@ -1079,6 +1263,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
if (res.data.options.expirationDate) {
|
||||
state.options.expirationDate = new Date(res.data.options.expirationDate);
|
||||
}
|
||||
selectedSourceKey = sourceKey(parseGithubUrl(state.sourceUrl));
|
||||
if (await restoreGitHubDraft()) return;
|
||||
await Promise.all([getRepoDetails(), getReadme()]);
|
||||
anonymizeReadme();
|
||||
@@ -1104,19 +1289,10 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
if (res.data.options.expirationDate) {
|
||||
state.options.expirationDate = new Date(res.data.options.expirationDate);
|
||||
}
|
||||
selectedSourceKey = sourceKey(parseGithubUrl(state.sourceUrl));
|
||||
if (await restoreGitHubDraft()) return;
|
||||
try {
|
||||
state.details = (await http.get(`/api/pr/${res.data.source.repositoryFullName}/${res.data.source.pullRequestId}`, { params: { connection: state.githubConnection } })).data;
|
||||
} catch (error) {
|
||||
const code = error && error.data && error.data.error;
|
||||
if (code) {
|
||||
translate("ERRORS." + code).then((translation) => {
|
||||
state.addToast({ title: "Error", date: new Date(), body: translation });
|
||||
state.error = translation;
|
||||
}, console.error);
|
||||
displayErrorMessage(code);
|
||||
}
|
||||
}
|
||||
try { await getPrDetails(); }
|
||||
catch (_) { /* The source handler displays the error. */ }
|
||||
|
||||
},
|
||||
() => { location.url("/404"); }
|
||||
@@ -1138,6 +1314,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
if (res.data.options.expirationDate) {
|
||||
state.options.expirationDate = new Date(res.data.options.expirationDate);
|
||||
}
|
||||
selectedSourceKey = sourceKey(parseGithubUrl(state.sourceUrl));
|
||||
if (await restoreGitHubDraft()) return;
|
||||
await getGistDetails();
|
||||
|
||||
@@ -1153,36 +1330,56 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
if (!params.repoId && !params.pullRequestId && !params.gistId && state.githubConnection === undefined
|
||||
&& res.data.appEnabled && res.data.appConnected) {
|
||||
state.githubConnection = "github-app";
|
||||
if (state.sourceUrl && state.workspaceOpen) refreshGitHubAccess();
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// URL change handler - auto-detect type
|
||||
// Keep the same input focused until a complete GitHub URL can be loaded.
|
||||
state.urlSelected = async (preserveDraft = false) => {
|
||||
const generation = ++sourceGeneration;
|
||||
let o;
|
||||
try { o = parseGithubUrl(state.sourceUrl.trim()); }
|
||||
catch (_) {
|
||||
state.sourceLoading = false;
|
||||
scheduleReadmePreview.cancel();
|
||||
refreshPrPreview.cancel();
|
||||
refreshGistPreview.cancel();
|
||||
state.previewStatus = "idle";
|
||||
state.sourceUrlError = state.sourceUrl ? "Please provide a valid GitHub URL." : "";
|
||||
setValidity("sourceUrl", "github", false);
|
||||
return;
|
||||
}
|
||||
const key = sourceKey(o);
|
||||
const preserveSelection = preserveDraft || selectedSourceKey === key;
|
||||
selectedSourceKey = key;
|
||||
state.sourceUrlError = "";
|
||||
setValidity("sourceUrl", "github", true);
|
||||
const transferFocus = document.activeElement?.id === "sourceUrl-landing";
|
||||
state.workspaceOpen = true;
|
||||
if (transferFocus) timeout(() => document.getElementById("sourceUrl")?.focus(), 0);
|
||||
state.reconnectingSource = false;
|
||||
state.reconnectRepositoryId = undefined;
|
||||
if (!preserveDraft) state.terms = state.defaultTerms;
|
||||
if (!preserveDraft && !state.isUpdate) {
|
||||
if (!preserveSelection && !state.isUpdate) {
|
||||
state.repoId = "";
|
||||
state.pullRequestId = "";
|
||||
state.gistId = "";
|
||||
}
|
||||
state.details = null;
|
||||
state.branches = [];
|
||||
if (!preserveDraft) state.source = { type: "GitHubStream", branch: "", commit: "" };
|
||||
if (!preserveSelection) state.source = { type: "GitHubStream", branch: "", commit: "" };
|
||||
scheduleReadmePreview.cancel();
|
||||
refreshPrPreview.cancel();
|
||||
refreshGistPreview.cancel();
|
||||
_prAnonCache.clear();
|
||||
_gistAnonCache.clear();
|
||||
state.previewStatus = "idle";
|
||||
state.anonymize_readme = "";
|
||||
state.readme = "";
|
||||
state.html_readme = "";
|
||||
state.detectedType = null;
|
||||
state.gistOAuthRequired = false;
|
||||
|
||||
let o;
|
||||
try {
|
||||
o = parseGithubUrl(state.sourceUrl);
|
||||
} catch (error) {
|
||||
setValidity("sourceUrl", "github", false);
|
||||
return;
|
||||
}
|
||||
setValidity("sourceUrl", "github", true);
|
||||
state._preservingDraft = preserveSelection;
|
||||
try {
|
||||
if (o.gistId && !o.repo) {
|
||||
state.detectedType = "gist";
|
||||
@@ -1197,10 +1394,8 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
await Promise.all([getRepoDetails(), getReadme()]);
|
||||
anonymizeReadme();
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (_) { /* Source errors are displayed by the request handlers. */ }
|
||||
finally { if (generation === sourceGeneration) state._preservingDraft = false; }
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
};
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
@@ -1231,10 +1426,12 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
|
||||
state.getBranches = async (force) => {
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const request = beginSourceRequest();
|
||||
try {
|
||||
const branches = await http.get(`/api/repo/${o.owner}/${o.repo}/branches`, {
|
||||
params: { reconnect: state.reconnectingSource ? "1" : undefined, anonymizedRepoId: state.isUpdate && params.repoId && parseRepoFullName(state.sourceUrl) === state._originalFullName ? params.repoId : undefined, connection: state.githubConnection, force: force === true || state.reconnectingSource ? "1" : "0", repositoryID: sourceRepositoryID() },
|
||||
});
|
||||
if (!isCurrentSource(request)) return;
|
||||
state.branches = branches.data;
|
||||
state.sourceUnreachable = false;
|
||||
if (!state.source.branch) {
|
||||
@@ -1259,6 +1456,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
await getReadme(force);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCurrentSource(request)) return;
|
||||
state.branches = [];
|
||||
state.sourceUnreachable = error && (error.status === 404 || (error.data && error.data.error === "repo_not_found"));
|
||||
const code = (error && error.data && error.data.error) || (error && error.status === 404 ? "repo_not_found" : "unknown_error");
|
||||
@@ -1270,12 +1468,13 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
if (typeof setValidity === "function") {
|
||||
setValidity("sourceUrl", "missing", false);
|
||||
}
|
||||
}
|
||||
} finally { finishSourceRequest(request); }
|
||||
|
||||
};
|
||||
|
||||
async function getRepoDetails() {
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const request = beginSourceRequest();
|
||||
try {
|
||||
resetValidity();
|
||||
// force=1 so newly enabled features (e.g. GitHub Pages — see
|
||||
@@ -1284,6 +1483,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
const res = await http.get(`/api/repo/${o.owner}/${o.repo}/`, {
|
||||
params: { reconnect: state.reconnectingSource ? "1" : undefined, anonymizedRepoId: state.isUpdate && params.repoId && parseRepoFullName(state.sourceUrl) === state._originalFullName ? params.repoId : undefined, connection: state.githubConnection, repositoryID: sourceRepositoryID(), force: "1" },
|
||||
});
|
||||
if (!isCurrentSource(request)) return;
|
||||
state.details = res.data;
|
||||
if (state.reconnectingSource) state.reconnectRepositoryId = state.details.externalId;
|
||||
if (state.details && state.details.id) {
|
||||
@@ -1294,6 +1494,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
}
|
||||
await state.getBranches(state.reconnectingSource);
|
||||
} catch (error) {
|
||||
if (!isCurrentSource(request)) return;
|
||||
if (error.data) {
|
||||
translate("ERRORS." + error.data.error).then((translation) => {
|
||||
state.addToast({ title: "Error", date: new Date(), body: translation });
|
||||
@@ -1303,20 +1504,21 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
}
|
||||
setValidity("sourceUrl", "missing", false);
|
||||
throw error;
|
||||
}
|
||||
} finally { finishSourceRequest(request); }
|
||||
}
|
||||
|
||||
async function getReadme(force) {
|
||||
if (state.readme && !force) return state.readme;
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const request = beginSourceRequest();
|
||||
try {
|
||||
const res = await http.get(`/api/repo/${o.owner}/${o.repo}/readme`, {
|
||||
params: { reconnect: state.reconnectingSource ? "1" : undefined, anonymizedRepoId: state.isUpdate && params.repoId && parseRepoFullName(state.sourceUrl) === state._originalFullName ? params.repoId : undefined, connection: state.githubConnection, force: force === true ? "1" : "0", branch: state.source.branch, repositoryID: sourceRepositoryID() },
|
||||
});
|
||||
state.readme = res.data;
|
||||
if (isCurrentSource(request, true)) state.readme = res.data;
|
||||
} catch (error) {
|
||||
state.readme = "";
|
||||
}
|
||||
if (isCurrentSource(request, true)) state.readme = "";
|
||||
} finally { finishSourceRequest(request); }
|
||||
}
|
||||
|
||||
// Both anonymizeReadme() and anonymizePrContent() used to reimplement
|
||||
@@ -1344,36 +1546,51 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
return opts;
|
||||
}
|
||||
|
||||
// Single-flight + debounced wrapper. Returns a promise that resolves
|
||||
// with the latest server result; intermediate calls are coalesced.
|
||||
function makePreviewBatcher(buildBody, applyResult) {
|
||||
// Invalidate as soon as an option changes, before the debounce expires.
|
||||
function makePreviewBatcher(kind, buildBody, applyResult) {
|
||||
let pendingTimer = null;
|
||||
let inflightToken = 0;
|
||||
return function schedule() {
|
||||
let generation = 0;
|
||||
const schedule = () => {
|
||||
if (state.detectedType !== kind) return;
|
||||
if (pendingTimer) timeout.cancel(pendingTimer);
|
||||
const current = ++generation;
|
||||
state.previewStatus = "loading";
|
||||
pendingTimer = timeout(() => {
|
||||
pendingTimer = null;
|
||||
const myToken = ++inflightToken;
|
||||
const body = buildBody();
|
||||
if (!body) return;
|
||||
http.post("/api/anonymize-preview", body).then(
|
||||
(res) => {
|
||||
if (myToken !== inflightToken) return; // stale
|
||||
applyResult(res.data);
|
||||
},
|
||||
() => { /* ignore preview errors; no UI feedback needed */ }
|
||||
);
|
||||
if (!body) { state.previewStatus = "idle"; return; }
|
||||
http.post("/api/anonymize-preview", body).then(res => {
|
||||
if (current !== generation) return;
|
||||
applyResult(res.data, body);
|
||||
state.previewStatus = "ready";
|
||||
}).catch(() => {
|
||||
if (current === generation) state.previewStatus = "error";
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
schedule.cancel = () => {
|
||||
generation++;
|
||||
if (pendingTimer) timeout.cancel(pendingTimer);
|
||||
pendingTimer = null;
|
||||
};
|
||||
state.on("dispose", schedule.cancel);
|
||||
return schedule;
|
||||
}
|
||||
state.retryPreview = () => {
|
||||
if (state.detectedType === "repo") scheduleReadmePreview();
|
||||
if (state.detectedType === "pr") refreshPrPreview();
|
||||
if (state.detectedType === "gist") refreshGistPreview();
|
||||
};
|
||||
|
||||
const scheduleReadmePreview = makePreviewBatcher(
|
||||
"repo",
|
||||
() => {
|
||||
if (!state.readme) return null;
|
||||
return { content: state.readme, options: previewOptions() };
|
||||
},
|
||||
(data) => {
|
||||
state.anonymize_readme = data.content || "";
|
||||
if (typeof data?.content !== "string") throw Error("Invalid preview response");
|
||||
state.anonymize_readme = data.content;
|
||||
let baseUrl = "";
|
||||
try {
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
@@ -1406,14 +1623,17 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
// ========== PR LOGIC ==========
|
||||
async function getPrDetails() {
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const request = beginSourceRequest();
|
||||
try {
|
||||
resetValidity();
|
||||
const res = await http.get(`/api/pr/${o.owner}/${o.repo}/${o.pullRequestId}`, { params: { connection: state.githubConnection } });
|
||||
if (!isCurrentSource(request)) return;
|
||||
state.details = res.data;
|
||||
if (!state.pullRequestId) {
|
||||
state.pullRequestId = o.repo + "-PR" + o.pullRequestId + "-" + generateRandomId(4);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCurrentSource(request)) return;
|
||||
if (error.data) {
|
||||
translate("ERRORS." + error.data.error).then((translation) => {
|
||||
state.addToast({ title: "Error", date: new Date(), body: translation });
|
||||
@@ -1423,16 +1643,11 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
}
|
||||
setValidity("sourceUrl", "missing", false);
|
||||
throw error;
|
||||
}
|
||||
} finally { finishSourceRequest(request); }
|
||||
}
|
||||
|
||||
// Angular templates evaluate this synchronously, so we keep a
|
||||
// {original -> anonymized} cache populated by a debounced batch call to
|
||||
// /api/anonymize-preview whenever the PR details, terms, or options
|
||||
// change. anonymizePrContent() returns the cached value if known and
|
||||
// falls back to the original until the next cycle resolves.
|
||||
// Original text is never substituted for a pending or failed preview.
|
||||
const _prAnonCache = reactive(new Map());
|
||||
let _prSeenContents = new Set();
|
||||
|
||||
function collectPrContents() {
|
||||
const out = new Set();
|
||||
@@ -1451,16 +1666,16 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
}
|
||||
|
||||
const refreshPrPreview = makePreviewBatcher(
|
||||
"pr",
|
||||
() => {
|
||||
const seen = collectPrContents();
|
||||
_prSeenContents = seen;
|
||||
const list = Array.from(seen);
|
||||
if (list.length === 0) return null;
|
||||
return { contents: list, options: previewOptions() };
|
||||
},
|
||||
(data) => {
|
||||
if (!data || !Array.isArray(data.contents)) return;
|
||||
const seen = Array.from(_prSeenContents);
|
||||
(data, body) => {
|
||||
if (!Array.isArray(data?.contents) || data.contents.length !== body.contents.length || data.contents.some(value => typeof value !== "string")) throw Error("Invalid preview response");
|
||||
const seen = body.contents;
|
||||
const next = new Map();
|
||||
for (let i = 0; i < seen.length && i < data.contents.length; i++) {
|
||||
next.set(seen[i], data.contents[i]);
|
||||
@@ -1473,15 +1688,13 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
state.anonymizePrContent = function (content) {
|
||||
if (!content) return content;
|
||||
if (_prAnonCache.has(content)) return _prAnonCache.get(content);
|
||||
if (!_prSeenContents.has(content)) {
|
||||
refreshPrPreview();
|
||||
}
|
||||
return content;
|
||||
return "";
|
||||
};
|
||||
|
||||
// ========== GIST LOGIC ==========
|
||||
async function getGistDetails() {
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const request = beginSourceRequest();
|
||||
try {
|
||||
resetValidity();
|
||||
state.gistOAuthRequired = false;
|
||||
@@ -1491,11 +1704,13 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
return;
|
||||
}
|
||||
const res = await http.get(`/api/gist/source/${o.gistId}`);
|
||||
if (!isCurrentSource(request)) return;
|
||||
state.details = res.data;
|
||||
if (!state.gistId) {
|
||||
state.gistId = "gist-" + o.gistId.substring(0, 6) + "-" + generateRandomId(4);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCurrentSource(request)) return;
|
||||
if (error.data?.error === "github_oauth_required") {
|
||||
state.gistOAuthRequired = true;
|
||||
setValidity("sourceUrl", "missing", false);
|
||||
@@ -1510,11 +1725,10 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
}
|
||||
setValidity("sourceUrl", "missing", false);
|
||||
throw error;
|
||||
}
|
||||
} finally { finishSourceRequest(request); }
|
||||
}
|
||||
|
||||
const _gistAnonCache = reactive(new Map());
|
||||
let _gistSeenContents = new Set();
|
||||
|
||||
function collectGistContents() {
|
||||
const out = new Set();
|
||||
@@ -1536,16 +1750,16 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
}
|
||||
|
||||
const refreshGistPreview = makePreviewBatcher(
|
||||
"gist",
|
||||
() => {
|
||||
const seen = collectGistContents();
|
||||
_gistSeenContents = seen;
|
||||
const list = Array.from(seen);
|
||||
if (list.length === 0) return null;
|
||||
return { contents: list, options: previewOptions() };
|
||||
},
|
||||
(data) => {
|
||||
if (!data || !Array.isArray(data.contents)) return;
|
||||
const seen = Array.from(_gistSeenContents);
|
||||
(data, body) => {
|
||||
if (!Array.isArray(data?.contents) || data.contents.length !== body.contents.length || data.contents.some(value => typeof value !== "string")) throw Error("Invalid preview response");
|
||||
const seen = body.contents;
|
||||
const next = new Map();
|
||||
for (let i = 0; i < seen.length && i < data.contents.length; i++) {
|
||||
next.set(seen[i], data.contents[i]);
|
||||
@@ -1559,10 +1773,7 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
state.anonymizeGistContent = function (content) {
|
||||
if (!content) return content;
|
||||
if (_gistAnonCache.has(content)) return _gistAnonCache.get(content);
|
||||
if (!_gistSeenContents.has(content)) {
|
||||
refreshGistPreview();
|
||||
}
|
||||
return content;
|
||||
return "";
|
||||
};
|
||||
|
||||
// Precomputed file objects for the preview pane so <gist-file>'s
|
||||
@@ -1731,9 +1942,15 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
|
||||
// Submit: repo
|
||||
state.anonymizeRepo = (event) => {
|
||||
if (state.sourceLoading) { state.error = "Wait for the source to finish loading before saving."; return; }
|
||||
if (state.reconnectingSource && !state.reconnectRepositoryId) return;
|
||||
if (expirationDateInvalid()) return;
|
||||
event.target.disabled = true;
|
||||
if (state.anonymize?.invalid || state.sourceUrlError) {
|
||||
state.error = "Please correct the highlighted fields before saving.";
|
||||
return;
|
||||
}
|
||||
const button = event.currentTarget || event.target.closest?.("button") || event.target;
|
||||
button.disabled = true;
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const payload = {
|
||||
repoId: state.repoId,
|
||||
@@ -1750,20 +1967,26 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
resetValidity();
|
||||
const url = state.isUpdate ? "/api/repo/" + state.repoId : "/api/repo/";
|
||||
http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then(
|
||||
() => { window.location.href = "/status/" + state.repoId; },
|
||||
() => { draft.clear(); window.location.href = "/status/" + state.repoId; },
|
||||
(error) => {
|
||||
if (error.data) {
|
||||
translate("ERRORS." + error.data.error).then((t) => { state.error = t; }, console.error);
|
||||
displayErrorMessage(error.data.error);
|
||||
}
|
||||
}
|
||||
).finally(() => { event.target.disabled = false; });
|
||||
).finally(() => { button.disabled = false; });
|
||||
};
|
||||
|
||||
// Submit: Gist
|
||||
state.anonymizeGist = (event) => {
|
||||
if (state.sourceLoading) { state.error = "Wait for the source to finish loading before saving."; return; }
|
||||
if (expirationDateInvalid()) return;
|
||||
event.target.disabled = true;
|
||||
if (state.anonymize?.invalid || state.sourceUrlError) {
|
||||
state.error = "Please correct the highlighted fields before saving.";
|
||||
return;
|
||||
}
|
||||
const button = event.currentTarget || event.target.closest?.("button") || event.target;
|
||||
button.disabled = true;
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const payload = {
|
||||
gistId: state.gistId,
|
||||
@@ -1775,20 +1998,26 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
resetValidity();
|
||||
const url = state.isUpdate ? "/api/gist/" + state.gistId : "/api/gist/";
|
||||
http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then(
|
||||
() => { window.location.href = "/gist/" + state.gistId; },
|
||||
() => { draft.clear(); window.location.href = "/gist/" + state.gistId; },
|
||||
(error) => {
|
||||
if (error.data) {
|
||||
translate("ERRORS." + error.data.error).then((t) => { state.error = t; }, console.error);
|
||||
displayErrorMessage(error.data.error);
|
||||
}
|
||||
}
|
||||
).finally(() => { event.target.disabled = false; });
|
||||
).finally(() => { button.disabled = false; });
|
||||
};
|
||||
|
||||
// Submit: PR
|
||||
state.anonymizePullRequest = (event) => {
|
||||
if (state.sourceLoading) { state.error = "Wait for the source to finish loading before saving."; return; }
|
||||
if (expirationDateInvalid()) return;
|
||||
event.target.disabled = true;
|
||||
if (state.anonymize?.invalid || state.sourceUrlError) {
|
||||
state.error = "Please correct the highlighted fields before saving.";
|
||||
return;
|
||||
}
|
||||
const button = event.currentTarget || event.target.closest?.("button") || event.target;
|
||||
button.disabled = true;
|
||||
const o = parseGithubUrl(state.sourceUrl);
|
||||
const payload = {
|
||||
pullRequestId: state.pullRequestId,
|
||||
@@ -1801,14 +2030,14 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
resetValidity();
|
||||
const url = state.isUpdate ? "/api/pr/" + state.pullRequestId : "/api/pr/";
|
||||
http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then(
|
||||
() => { window.location.href = "/pr/" + state.pullRequestId; },
|
||||
() => { draft.clear(); window.location.href = "/pr/" + state.pullRequestId; },
|
||||
(error) => {
|
||||
if (error.data) {
|
||||
translate("ERRORS." + error.data.error).then((t) => { state.error = t; }, console.error);
|
||||
displayErrorMessage(error.data.error);
|
||||
}
|
||||
}
|
||||
).finally(() => { event.target.disabled = false; });
|
||||
).finally(() => { button.disabled = false; });
|
||||
};
|
||||
|
||||
state.watch("conference", () => { getConference(); });
|
||||
@@ -2589,6 +2818,8 @@ export const conferencesController = function (state, http, location) {
|
||||
}
|
||||
|
||||
state.conferences = [];
|
||||
state.loading = true;
|
||||
state.error = "";
|
||||
state.search = "";
|
||||
|
||||
const conferencesPrefsKey = "conferences.filterPrefs";
|
||||
@@ -2624,11 +2855,13 @@ export const conferencesController = function (state, http, location) {
|
||||
);
|
||||
|
||||
state.removeConference = function (conf) {
|
||||
if (conf.removing) return;
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure that you want to remove the conference ${conf.name}? All the repositories linked to this conference will expire.`
|
||||
)
|
||||
) {
|
||||
conf.removing = true;
|
||||
const toast = reactive({
|
||||
title: `Removing ${conf.name}...`,
|
||||
date: new Date(),
|
||||
@@ -2638,30 +2871,36 @@ export const conferencesController = function (state, http, location) {
|
||||
http.delete(`/api/conferences/${conf.conferenceID}`).then(() => {
|
||||
toast.title = `${conf.name} is removed.`;
|
||||
toast.body = `The conference ${conf.name} is removed.`;
|
||||
getConferences();
|
||||
});
|
||||
state.loadConferences();
|
||||
}).catch(() => {
|
||||
toast.title = "Unable to remove conference";
|
||||
toast.body = "Please try again. The conference has not been removed.";
|
||||
}).finally(() => { conf.removing = false; });
|
||||
}
|
||||
};
|
||||
|
||||
function getConferences() {
|
||||
http.get("/api/conferences/").then(
|
||||
state.loadConferences = () => {
|
||||
state.loading = true;
|
||||
state.error = "";
|
||||
return http.get("/api/conferences/").then(
|
||||
(res) => {
|
||||
state.conferences = res.data || [];
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
state.error = "Unable to load your conferences. Please try again.";
|
||||
}
|
||||
);
|
||||
}
|
||||
getConferences();
|
||||
).finally(() => { state.loading = false; });
|
||||
};
|
||||
state.loadConferences();
|
||||
|
||||
state.conferenceFilter = (conference) => {
|
||||
if (state.filters.status[conference.status] == false) return false;
|
||||
|
||||
if (state.search.trim().length == 0) return true;
|
||||
const query = state.search.trim().toLowerCase();
|
||||
if (!query) return true;
|
||||
|
||||
if (conference.name.indexOf(state.search) > -1) return true;
|
||||
if (conference.conferenceID.indexOf(state.search) > -1) return true;
|
||||
if ((conference.name || "").toLowerCase().includes(query)) return true;
|
||||
if ((conference.conferenceID || "").toLowerCase().includes(query)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -2679,12 +2918,14 @@ export const newConferenceController = function (state, http, location, params)
|
||||
|
||||
state.plans = [];
|
||||
state.editionMode = false;
|
||||
state.saving = false;
|
||||
state.error = "";
|
||||
|
||||
function getConference() {
|
||||
http
|
||||
.get("/api/conferences/" + params.conferenceId)
|
||||
.then((res) => {
|
||||
state.options = res.data;
|
||||
state.options = { ...state.options, ...res.data, billing: res.data.billing || {} };
|
||||
state.options.startDate = new Date(state.options.startDate);
|
||||
state.options.endDate = new Date(state.options.endDate);
|
||||
});
|
||||
@@ -2710,6 +2951,7 @@ export const newConferenceController = function (state, http, location, params)
|
||||
const end = new Date(start);
|
||||
end.setMonth(start.getMonth() + 7, 0);
|
||||
state.options = {
|
||||
billing: {},
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
plan: {
|
||||
@@ -2777,39 +3019,44 @@ export const newConferenceController = function (state, http, location, params)
|
||||
}
|
||||
|
||||
state.submit = function () {
|
||||
if (state.saving) return;
|
||||
state.saving = true;
|
||||
state.error = "";
|
||||
const submitted = JSON.parse(JSON.stringify(state.options));
|
||||
const toast = reactive({
|
||||
title: `Creating ${state.options.name}...`,
|
||||
title: `Creating ${submitted.name}...`,
|
||||
date: new Date(),
|
||||
body: `The conference ${state.options.conferenceID} is in creation.`,
|
||||
body: `The conference ${submitted.conferenceID} is in creation.`,
|
||||
});
|
||||
if (state.editionMode) {
|
||||
toast.title = `Updating ${state.options.name}...`;
|
||||
toast.body = `The conference '${state.options.conferenceID}' is updating.`;
|
||||
toast.title = `Updating ${submitted.name}...`;
|
||||
toast.body = `The conference '${submitted.conferenceID}' is updating.`;
|
||||
}
|
||||
state.addToast(toast);
|
||||
resetValidity();
|
||||
http
|
||||
.post(
|
||||
"/api/conferences/" +
|
||||
(state.editionMode ? state.options.conferenceID : ""),
|
||||
state.options
|
||||
(state.editionMode ? submitted.conferenceID : ""),
|
||||
submitted
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
if (!state.editionMode) {
|
||||
toast.title = `${state.options.name} created`;
|
||||
toast.body = `The conference '${state.options.conferenceID}' is created.`;
|
||||
toast.title = `${submitted.name} created`;
|
||||
toast.body = `The conference '${submitted.conferenceID}' is created.`;
|
||||
} else {
|
||||
toast.title = `${state.options.name} updated`;
|
||||
toast.body = `The conference '${state.options.conferenceID}' is updated.`;
|
||||
toast.title = `${submitted.name} updated`;
|
||||
toast.body = `The conference '${submitted.conferenceID}' is updated.`;
|
||||
}
|
||||
location.url("/conference/" + state.options.conferenceID);
|
||||
location.url("/conference/" + submitted.conferenceID);
|
||||
},
|
||||
(error) => {
|
||||
displayErrorMessage(error.data.error);
|
||||
displayErrorMessage(error.data?.error);
|
||||
state.error = "Unable to save the conference. Please correct any highlighted fields and try again.";
|
||||
state.removeToast(toast);
|
||||
}
|
||||
);
|
||||
).finally(() => { state.saving = false; });
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2833,10 +3080,11 @@ export const conferenceController = function (state, http, location, params) {
|
||||
state.repoFiler = (repo) => {
|
||||
if (state.filters.status[repo.status] == false) return false;
|
||||
|
||||
if (state.search.trim().length == 0) return true;
|
||||
const query = state.search.trim().toLowerCase();
|
||||
if (!query) return true;
|
||||
|
||||
if (repo.source.fullName.indexOf(state.search) > -1) return true;
|
||||
if (repo.repoId.indexOf(state.search) > -1) return true;
|
||||
if ((repo.source?.fullName || "").toLowerCase().includes(query)) return true;
|
||||
if ((repo.repoId || "").toLowerCase().includes(query)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,16 @@ 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";
|
||||
import { render as quotaUsageTemplate } from "../partials/quotaUsage.htm";
|
||||
|
||||
const QuotaUsage = {
|
||||
props: ["quota", "fmt"],
|
||||
setup(props) {
|
||||
return { formatAmount: (value, kind) => kind === "bytes"
|
||||
? props.fmt.humanFileSize(value).replace(/([a-zA-Z]+)$/, " $1") : props.fmt.number(value) };
|
||||
},
|
||||
render: quotaUsageTemplate,
|
||||
};
|
||||
|
||||
const editorModes = new Set(__ACE_MODES__);
|
||||
|
||||
@@ -68,7 +78,7 @@ const Loc = {
|
||||
};
|
||||
},
|
||||
};
|
||||
export const components = { Markdown, GistFile, Notebook, Loc, HtmlDoc, Pdfviewer: defineAsyncComponent(async () => {
|
||||
export const components = { Markdown, GistFile, Notebook, Loc, HtmlDoc, QuotaUsage, Pdfviewer: defineAsyncComponent(async () => {
|
||||
await loadLibrary("pdf");
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "/script/external/pdf.worker.js";
|
||||
return PdfViewer;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Drafts stay in this browser tab and are tied to the signed-in account.
|
||||
export function formDraft(state, fields, key) {
|
||||
let baseline;
|
||||
let watching = false;
|
||||
const storageKey = "form-draft:" + key;
|
||||
const values = () => Object.fromEntries(fields.map(field => [field, state[field]]));
|
||||
const serialize = () => JSON.stringify(values());
|
||||
const apply = (saved) => {
|
||||
for (const field of fields) {
|
||||
if (Object.prototype.hasOwnProperty.call(saved, field)) state[field] = saved[field];
|
||||
}
|
||||
if (state.options?.expirationDate) state.options.expirationDate = new Date(state.options.expirationDate);
|
||||
};
|
||||
const remove = () => { try { window.sessionStorage.removeItem(storageKey); } catch { /* Storage may be disabled. */ } };
|
||||
const save = (flush = false) => {
|
||||
if (baseline === undefined) return;
|
||||
if (flush) document.querySelectorAll("input, select, textarea").forEach(el => {
|
||||
if (el._field?.binding.state === state && el._field.timer) el._field.commit();
|
||||
});
|
||||
state.unsavedChanges = serialize() !== baseline;
|
||||
state.draftSaved = false;
|
||||
if (!state.unsavedChanges) { remove(); return; }
|
||||
try {
|
||||
window.sessionStorage.setItem(storageKey, JSON.stringify({
|
||||
account: state.user?.username || null, savedAt: Date.now(), values: values(),
|
||||
}));
|
||||
state.draftSaved = true;
|
||||
} catch { /* Keep the form usable when storage is disabled or full. */ }
|
||||
};
|
||||
const saveBeforeLeaving = () => save(true);
|
||||
state.on("routeLeave", saveBeforeLeaving);
|
||||
window.addEventListener("beforeunload", saveBeforeLeaving);
|
||||
state.on("dispose", () => window.removeEventListener("beforeunload", saveBeforeLeaving));
|
||||
return {
|
||||
restore() {
|
||||
baseline = serialize();
|
||||
let saved;
|
||||
try { saved = JSON.parse(window.sessionStorage.getItem(storageKey) || "null"); } catch { /* Ignore invalid drafts. */ }
|
||||
const restored = saved && saved.account === (state.user?.username || null)
|
||||
&& Date.now() - saved.savedAt < 30 * 60000 && saved.values;
|
||||
if (restored) {
|
||||
apply(saved.values);
|
||||
state.draftRestored = true;
|
||||
}
|
||||
if (!watching) {
|
||||
watching = true;
|
||||
state.watch(() => values(), () => save(), true);
|
||||
}
|
||||
state.unsavedChanges = serialize() !== baseline;
|
||||
return !!restored;
|
||||
},
|
||||
checkpoint() { return JSON.parse(serialize()); },
|
||||
accept(saved) {
|
||||
baseline = JSON.stringify(saved);
|
||||
state.draftRestored = false;
|
||||
save();
|
||||
},
|
||||
discard() {
|
||||
document.querySelectorAll("input, select, textarea").forEach(el => {
|
||||
if (el._field?.binding.state !== state || !el._field.timer) return;
|
||||
clearTimeout(el._field.timer);
|
||||
el._field.timer = null;
|
||||
el.value = el._field.binding.value ?? "";
|
||||
});
|
||||
if (baseline !== undefined) apply(JSON.parse(baseline));
|
||||
state.draftRestored = false;
|
||||
save();
|
||||
},
|
||||
clear() {
|
||||
baseline = serialize();
|
||||
state.unsavedChanges = false;
|
||||
state.draftRestored = false;
|
||||
state.draftSaved = false;
|
||||
remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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 { render as iconsTemplate } from "../partials/icons.htm";
|
||||
import { initializeTemplate } from "./template-state.js";
|
||||
import { createPageState, createTimers } from "./state.js";
|
||||
import { createHttp, promises } from "./http.js";
|
||||
@@ -100,6 +101,7 @@ export function mountApplication(target = "#app", options = {}) {
|
||||
document.addEventListener("click", navigate);
|
||||
onBeforeUnmount(() => document.removeEventListener("click", navigate));
|
||||
return () => [
|
||||
iconsTemplate(root, []),
|
||||
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,
|
||||
|
||||
@@ -5,7 +5,12 @@ 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/anonymize.htm") {
|
||||
state.prTabState = { active: state.options.diff ? "diff" : "comments" };
|
||||
state.watchGroup(["options.diff", "options.comments"], () => {
|
||||
if (!state.options[state.prTabState.active]) state.prTabState.active = state.options.diff ? "diff" : state.options.comments ? "comments" : null;
|
||||
});
|
||||
}
|
||||
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" };
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
"use strict";(()=>{var Hl=Object.defineProperty;var Bl=(e,t)=>{for(var o in t)Hl(e,o,{get:t[o],enumerable:!0})};var ye=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ge={org:"/script/org.f61f262915.min.js",notebook:"/script/notebook.c3144c18ae.min.js",editor:"/script/editor.4f142d3adc.min.js",markdown:"/script/markdown.44f2e93820.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js"};var an=new Map,Wl=ge;function Kt(e){if(!an.has(e)){let t=document.createElement("script");t.src=Wl[e];let o=new Promise((r,n)=>{t.onload=r,t.onerror=()=>{an.delete(e),t.remove(),n(new Error(`Unable to load ${e}. Please retry.`))}});an.set(e,o),document.head.appendChild(t)}return an.get(e)}async function sr(){await Kt("editor"),ace.config.set("basePath","/script/external/ace/")}function us(e){let t=Object.create(null);for(let o of e.split(","))t[o]=1;return o=>o in t}var $e={},Yt=[],it=()=>{},Zn=()=>!1,cs=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ps=e=>e.startsWith("onUpdate:"),je=Object.assign,Ds=(e,t)=>{let o=e.indexOf(t);o>-1&&e.splice(o,1)},Kl=Object.prototype.hasOwnProperty,Ie=(e,t)=>Kl.call(e,t),Ce=Array.isArray,qt=e=>Rs(e)==="[object Map]",ds=e=>Rs(e)==="[object Set]",nr=e=>Rs(e)==="[object Date]";var Ee=e=>typeof e=="function",ze=e=>typeof e=="string",pt=e=>typeof e=="symbol",Pe=e=>e!==null&&typeof e=="object",xn=e=>(Pe(e)||Ee(e))&&Ee(e.then)&&Ee(e.catch),ir=Object.prototype.toString,Rs=e=>ir.call(e),eo=e=>Rs(e).slice(8,-1),ln=e=>Rs(e)==="[object Object]",dn=e=>ze(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,fs=us(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var un=e=>{let t=Object.create(null);return(o=>t[o]||(t[o]=e(o)))},Yl=/-\w/g,Ye=un(e=>e.replace(Yl,t=>t.slice(1).toUpperCase())),Jl=/\B([A-Z])/g,Mt=un(e=>e.replace(Jl,"-$1").toLowerCase()),Jt=un(e=>e.charAt(0).toUpperCase()+e.slice(1)),Ts=un(e=>e?`on${Jt(e)}`:""),vt=(e,t)=>!Object.is(e,t),Os=(e,...t)=>{for(let o=0;o<e.length;o++)e[o](...t)},cn=(e,t,o,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:o})},to=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var or,As=()=>or||(or=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function qe(e){if(Ce(e)){let t={};for(let o=0;o<e.length;o++){let r=e[o],n=ze(r)?xl(r):qe(r);if(n)for(let i in n)t[i]=n[i]}return t}else if(ze(e)||Pe(e))return e}var Ql=/;(?![^(]*\))/g,Xl=/:([^]+)/,Zl=/\/\*[^]*?\*\//g;function xl(e){let t={};return e.replace(Zl,"").split(Ql).forEach(o=>{if(o){let r=o.split(Xl);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function O(e){let t="";if(ze(e))t=e;else if(Ce(e))for(let o=0;o<e.length;o++){let r=O(e[o]);r&&(t+=r+" ")}else if(Pe(e))for(let o in e)e[o]&&(t+=o+" ");return t.trim()}var ar="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",lr=us(ar),ed=us(ar+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function pn(e){return!!e||e===""}function td(e,t){if(e.length!==t.length)return!1;let o=!0;for(let r=0;o&&r<e.length;r++)o=ms(e[r],t[r]);return o}function rr(e,t){if(e.size!==t.size)return!1;let o=Array.from(t),r=new Uint8Array(o.length);for(let n of e){let i=-1;for(let a=0;a<o.length;a++)if(!r[a]&&ms(n,o[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function ms(e,t){if(e===t)return!0;let o=nr(e),r=nr(t);if(o||r)returLine truncated
|
||||
"use strict";(()=>{var xl=Object.defineProperty;var ed=(e,t)=>{for(var n in t)xl(e,n,{get:t[n],enumerable:!0})};var pe=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var fe={notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js",markdown:"/script/markdown.44f2e93820.min.js",editor:"/script/editor.4f142d3adc.min.js"};var us=new Map,sd=fe;function Jt(e){if(!us.has(e)){let t=document.createElement("script");t.src=sd[e];let n=new Promise((r,s)=>{t.onload=r,t.onerror=()=>{us.delete(e),t.remove(),s(new Error(`Unable to load ${e}. Please retry.`))}});us.set(e,n),document.head.appendChild(t)}return us.get(e)}async function lr(){await Jt("editor"),ace.config.set("basePath","/script/external/ace/")}function po(e){let t=Object.create(null);for(let n of e.split(","))t[n]=1;return n=>n in t}var Ie={},Qt=[],at=()=>{},sn=()=>!1,fo=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),mo=e=>e.startsWith("onUpdate:"),Ge=Object.assign,Oo=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},nd=Object.prototype.hasOwnProperty,De=(e,t)=>nd.call(e,t),be=Array.isArray,$t=e=>Io(e)==="[object Map]",co=e=>Io(e)==="[object Set]",dr=e=>Io(e)==="[object Date]";var we=e=>typeof e=="function",ze=e=>typeof e=="string",mt=e=>typeof e=="symbol",Ae=e=>e!==null&&typeof e=="object",nn=e=>(Ae(e)||we(e))&&we(e.then)&&we(e.catch),pr=Object.prototype.toString,Io=e=>pr.call(e),rn=e=>Io(e).slice(8,-1),cs=e=>Io(e)==="[object Object]",ps=e=>ze(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ho=po(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var fs=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},rd=/-\w/g,Je=fs(e=>e.replace(rd,t=>t.slice(1).toUpperCase())),id=/\B([A-Z])/g,St=fs(e=>e.replace(id,"-$1").toLowerCase()),Xt=fs(e=>e.charAt(0).toUpperCase()+e.slice(1)),Po=fs(e=>e?`on${Xt(e)}`:""),gt=(e,t)=>!Object.is(e,t),vo=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},ms=(e,t,n,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},Vo=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var ur,qo=()=>ur||(ur=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Me(e){if(be(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],s=ze(r)?ud(r):Me(r);if(s)for(let i in s)t[i]=s[i]}return t}else if(ze(e)||Ae(e))return e}var ad=/;(?![^(]*\))/g,ld=/:([^]+)/,dd=/\/\*[^]*?\*\//g;function ud(e){let t={};return e.replace(dd,"").split(ad).forEach(n=>{if(n){let r=n.split(ld);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function A(e){let t="";if(ze(e))t=e;else if(be(e))for(let n=0;n<e.length;n++){let r=A(e[n]);r&&(t+=r+" ")}else if(Ae(e))for(let n in e)e[n]&&(t+=n+" ");return t.trim()}var fr="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",mr=po(fr),cd=po(fr+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function hs(e){return!!e||e===""}function pd(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&&r<e.length;r++)n=go(e[r],t[r]);return n}function cr(e,t){if(e.size!==t.size)return!1;let n=Array.from(t),r=new Uint8Array(n.length);for(let s of e){let i=-1;for(let a=0;a<n.length;a++)if(!r[a]&&go(s,n[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function go(e,t){if(e===t)return!0;let n=dr(e),r=dr(t);if(n||r)returLine truncated
|
||||
`))),e[Bs]=ji(s);let i=r||s.props&&s.props.type==="number";So(e,t?"change":"input",a=>{a.target.composing||e[Bs](Fn(e.value,n,i))}),(n||i)&&So(e,"change",()=>{e.value=Fn(e.value,n,i)}),t||(So(e,"compositionstart",oc),So(e,"compositionend",Gi),So(e,"change",Gi))},mounted(e,{value:t,modifiers:{trim:n,number:r}}){let s=t??"",i=e[js];delete e[js],i!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==i?e[Bs](Fn(e.value,n,r)):e.value=s},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:s,number:i}},a){if(e[Bs]=ji(a),e.composing)return;let u=(i||e.type==="number")&&!/^0\d/.test(e.value)?Vo(e.value):e.value,m=t??"";if(u===m)return;let h=e.getRootNode();(h instanceof Document||h instanceof ShadowRoot)&&h.activeElement===e&&e.type!=="range"&&(r&&t===n||s&&e.value.trim()===m)||(e.value=m)}};var sc=["ctrl","shift","alt","meta"],nc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>sc.some(n=>e[`${n}Key`]&&!t.includes(n))},ce=(e,t)=>{if(!e)return e;let n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=((s,...i)=>{for(let a=0;a<t.length;a++){let u=nc[t[a]];if(u&&u(s,t))return}return e(s,...i)}))},rc={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},No=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=(s=>{if(!("key"in s))return;let i=St(s.key);if(t.some(a=>a===i||rc[a]===i))return e(s)}))},ic=Ge({patchProp:xu},$u),Wi;function ac(){return Wi||(Wi=Ni(ic))}var Qi=((...e)=>{let t=ac().createApp(...e),{mount:n}=t;return t.mount=r=>{let s=dc(r);if(!s)return;let i=t._component;!we(i)&&!i.render&&!i.template&&(i.template=s.innerHTML),s.nodeType===1&&(s.textContent="");let a=n(s,!1,lc(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),a},t});function lc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function dc(e){return ze(e)?document.querySelector(e):e}var lo=typeof document<"u";function Zi(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function uc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Zi(e.default)}var Oe=Object.assign;function Ws(e,t){let n={};for(let r in t){let s=t[r];n[r]=wt(s)?s.map(e):e(s)}return n}var Ro=()=>{},wt=Array.isArray;function jn(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var xi=/#/g,cc=/&/g,pc=/\//g,fc=/=/g,mc=/\?/g,ea=/\+/g,hc=/%5B/g,vc=/%5D/g,ta=/%5E/g,gc=/%60/g,oa=/%7B/g,yc=/%7C/g,sa=/%7D/g,bc=/%20/g;function Gn(e){return e==null?"":encodeURI(""+e).replace(yc,"|").replace(hc,"[").replace(vc,"]")}function na(e){return Gn(e).replace(oa,"{").replace(sa,"}").replace(ta,"^")}function Hn(e){return Gn(e).replace(ea,"%2B").replace(bc,"+").replace(xi,"%23").replace(cc,"%26").replace(gc,"`").replace(oa,"{").replace(sa,"}").replace(ta,"^")}function wc(e){return Hn(e).replace(fc,"%3D")}function kc(e){return Gn(e).replace(xi,"%23").replace(mc,"%3F")}function ra(e){return kc(e).replace(pc,"%2F")}function Do(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}var Ec=/\/$/,Cc=e=>e.replace(Ec,"");function Ks(e,t,n="/"){let r,s={},i="",a="",u=t.indexOf("#"),m=t.indexOf("?");return m=u>=0&&m>u?-1:m,m>=0&&(r=t.slice(0,m),i=t.slice(m,u>0?u:t.length),s=e(i.slice(1))),u>=0&&(r=r||t.slice(0,u),a=t.slice(u,t.length)),r=Sc(r??t,n),{fullPath:r+i+a,path:r,query:s,hash:Do(a)}}function ia(e,t){let n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Wn(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function aa(e,t,n){let r=t.matched.length-1,s=n.matched.length-1;return r>-1&&r===s&&io(t.matched[r],n.matched[s])&&Kn(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function io(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Kn(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!_c(e[n],t[n]))return!1;return!0}function _c(e,t){return wt(e)?Xi(e,t):wt(t)?Xi(t,e):e?.valueOf()===t?.valueOf()}function Xi(e,t){return wt(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function Sc(e,t){if(e.startsWith("/"))return e;if(!e)return t;let n=t.split("/"),r=e.split("/"),s=r[r.length-1];(s===".."||s===".")&&r.push("");let i=n.length-1,a,u;for(a=0;a<r.length;a++)if(u=r[a],u!==".")if(u==="..")i>1&&i--;else break;return n.slice(0,i).join("/")+"/"+r.slice(a).join("/")}var Wt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},Ys=(function(e){return e.pop="pop",e.push="push",e})({}),Js=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function la(e){if(!e)if(lo){let t=document.querLine truncated
|
||||
${JSON.stringify(e)}${t?`
|
||||
while being at
|
||||
`+JSON.stringify(t):""}`},[Ge.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${gc(t)}" via a navigation guard.`},[Ge.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[Ge.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[Ge.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function rs(e,t){return Me(new Error,{type:e,[oa]:!0},t)}function Pt(e,t){return e instanceof Error&&oa in e&&(t==null||!!(e.type&t))}var yc=["params","query","hash"];function gc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let o of yc)o in e&&(t[o]=e[o]);return JSON.stringify(t,null,2)}function ra(e){let t={};if(e===""||e==="?")return t;let o=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<o.length;++r){let n=o[r].replace(ji," "),i=n.indexOf("="),a=Es(i<0?n:n.slice(0,i)),u=i<0?null:Es(n.slice(i+1));if(a in t){let m=t[a];bt(m)||(m=t[a]=[m]),m.push(u)}else t[a]=u}return t}function jo(e){let t="";for(let o in e){let r=e[o];if(o=lc(o),r==null){r!==void 0&&(t+=(t.length?"&":"")+o);continue}(bt(r)?r.map(n=>n&&$o(n)):[r&&$o(r)]).forEach(n=>{n!==void 0&&(t+=(t.length?"&":"")+o,n!=null&&(t+="="+n))})}return t}function ia(e){let t={};for(let o in e){let r=e[o];r!==void 0&&(t[o]=bt(r)?r.map(n=>n==null?null:""+n):r==null?r:""+r)}return t}var aa=Symbol(""),Wo=Symbol(""),jn=Symbol(""),Wn=Symbol(""),Kn=Symbol("");function Ns(){let e=[];function t(r){return e.push(r),()=>{let n=e.indexOf(r);n>-1&&e.splice(n,1)}}function o(){e=[]}return{add:t,list:()=>e.slice(),reset:o}}function Gt(e,t,o,r,n,i=a=>a()){let a=r&&(r.enterCallbacks[n]=r.enterCallbacks[n]||[]);return()=>new Promise((u,m)=>{let h=p=>{p===!1?m(rs(Ge.NAVIGATION_ABORTED,{from:o,to:t})):p instanceof Error?m(p):vc(p)?m(rs(Ge.NAVIGATION_GUARD_REDIRECT,{from:t,to:p})):(a&&r.enterCallbacks[n]===a&&typeof p=="function"&&a.push(p),u())},v=i(()=>e.call(r&&r.instances[n],t,o,h)),k=Promise.resolve(v);e.length<3&&(k=k.then(h)),k.catch(p=>m(p))})}function Yn(e,t,o,r,n=i=>i()){let i=[];for(let a of e)for(let u in a.components){let m=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(Bi(m)){let h=(m.__vccOpts||m)[t];h&&i.push(Gt(h,o,r,a,u,n))}else{let h=m();i.push(()=>h.then(v=>{if(!v)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let k=Zu(v)?v.default:v;a.mods[u]=v,a.components[u]=k;let p=(k.__vccOpts||k)[t];return p&&Gt(p,o,r,a,u,n)()}))}}return i}function la(e,t){let o=[],r=[],n=[],i=Math.max(t.matched.length,e.matched.length);for(let a=0;a<i;a++){let u=t.matched[a];u&&(e.matched.find(h=>os(h,u))?r.push(u):o.push(u));let m=e.matched[a];m&&(t.matched.find(h=>os(h,m))||n.push(m))}return[o,r,n]}var bc=()=>location.protocol+"//"+location.host;function ba(e,t){let{pathname:o,search:r,hash:n}=t,i=e.indexOf("#");if(i>-1){let a=n.includes(e.slice(i))?e.slice(i).length:1,u=n.slice(a);return u[0]!=="/"&&(u="/"+u),zo(u,"")}return zo(o,e)+r+n}function wc(e,t,o,r){let n=[],i=[],a=null,u=({state:p})=>{let g=ba(e,location),w=o.value,b=t.value,T=0;if(p){if(o.value=g,t.value=p,a&&a===w){a=null;return}T=b?p.position-b.position:0}else r(g);n.forEach(N=>{N(o.value,w,{delta:T,type:Bn.pop,direction:T?T>0?Gn.forward:Gn.back:Gn.unknown})})};function m(){a=o.value}function h(p){n.push(p);let g=()=>{let w=n.indexOf(p);w>-1&&n.splice(w,1)};return i.push(g),g}function v(){if(document.visibilityState==="hidden"){let{history:p}=window;if(!p.state)return;p.replaceState(Me({},p.state,{scroll:rn()}),"")}}function k(){for(let p of i)p();i=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",v),document.removeEventListener("visibilitychange",v)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",v),document.addEventListener("visibilitychange",v),{pauseListeners:m,listen:h,destroy:k}}function da(e,t,o,r=!1,n=!1){return{back:e,current:t,forward:o,replaced:r,position:window.history.length,scroll:n?rn():null}}function kc(e){let{history:t,location:o}=window,r={value:ba(e,o)},n={value:t.state};n.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(m,h,v){let k=e.indexOf("#"),p=k>-1?(o.host&&document.querySelector("base")?e:e.slice(k))+m:bc()+e+m;try{t[v?"replaceState":"pushState"](h,"",p),n.value=h}catch(g){console.error(g),o[v?"replace":"assign"](p)}}function a(m,h){i(m,Me({},t.state,da(n.value.back,m,n.value.forward,!0),h,{position:n.value.position}),!0),r.value=m}function u(m,h){let v=Me({},n.value,t.state,{forward:m,scroll:rn()});i(v.current,v,!0),i(m,Me({},da(r.value,m,null),{position:v.position+1},h),!1),r.value=m}return{location:r,state:n,push:u,replace:a}}function wa(e){e=xi(e);let t=kc(e),o=wc(e,t.state,t.location,t.replace);function r(i,a=!0){a||o.pauseListeners(),history.go(i)}let n=Me({locatLine truncated
|
||||
`)),e.options=Object.assign({},e.options,m.options)})}i();let a=null;e.saveDefault=u=>{u&&u.preventDefault&&u.preventDefault();let m={terms:e.terms.split(`
|
||||
`).map(h=>h.trim()).filter(h=>h.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",m).then(()=>{i(),e.saving=!1,e.message="Saved",a&&r.cancel(a),a=r(()=>{e.message=null},2500)},h=>{e.saving=!1;let v=h&&h.data&&h.data.error;o("ERRORS."+v).then(k=>{e.error=k},()=>{e.error="Unable to save your defaults. Please try again."})})},e.deleteAccount=()=>{confirm("Delete your account? All your anonymized repositories, gists, and pull requests will be removed, and your personal data will be erased. This cannot be undone.")&&(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},Ta=function(e,t,o){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(r=>{o.url("/dashboard")},r=>{e.error=r.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Oa=function(e,t,o,r,n){e.user&&!e.user.status&&o.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&o.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,m){let h={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(h===void 0)return;u.preventDefault();let v=e.features.length,k=h==="first"?0:h==="last"?v-1:(m+h+v)%v;e.feature=e.features[k].key,n(()=>{let p=r.document.getElementById("feature-tab-"+e.feature);p&&p.focus()})},e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function i(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests})}i();function a(u){let m={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return m;let h=new Array(u.length-1);for(let b=1;b<u.length;b++)h[b-1]=u[b]-u[b-1];let v=h.length,k=Math.max.apply(null,h),p=Math.min.apply(null,h),g=Math.min(0,p),w=k-g||1;m.viewW=v*2,m.bars=new Array(v);for(let b=0;b<v;b++){let T=(h[b]-g)/w,N=Math.max(1.5,T*34);m.bars[b]={x:(b*2+.25).toFixed(2),y:(36-N).toFixed(2),w:"1.5",h:N.toFixed(2)}}if(m.deltaToday=h[v-1],v>=2){let b=h[v-2];b&&(m.pctChange=(m.deltaToday-b)/b*100)}return m.pctAbs=Math.round(Math.abs(m.pctChange)),m.isUp=m.pctChange>=0,m}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let m=u.data||[];e.history={repositories:a(m.map(h=>h.nbRepositories||0)),users:a(m.map(h=>h.nbUsers||0)),pageViews:a(m.map(h=>h.nbPageViews||0)),pullRequests:a(m.map(h=>h.nbPullRequests||0))}})},Aa=function(e,t,o,r,n,i){let a=ht();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.items=[],e.search="",e.loading=!0,e.statusKeyLabels={ready:"Ready",progress:"In progress",error:"Error",expired:"Expired",removed:"Removed"};let u=["queue","download","downloaded","preparing","anonymizing"];function m(C){return C==="ready"||C==="error"?C:C==="expired"||C==="expiring"?"expired":C==="
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,W.options),e.options.expirationDate=W.options&&W.options.expirationDate?new Date(W.options.expirationDate):m(),D&&D()})}function p(D,V,W){e.anonymize&&e.anonymize[D]&&e.anonymize[D].setValidity(V,W)}function g(D){try{let V=parseGithubUrl(D);if(V&&V.owner&&V.repo)return V.owner+"/"+V.repo}catch{}return null}e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await N()}catch{}}};function w(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:g(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function b(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function T(){let D;try{D=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!D||D.path!==n.path()||Date.now()-D.savedAt>=30*6e4)return!1;for(let V of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(D.draft,V)&&(e[V]=D.draft[V]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await b(),sessionStorage.removeItem("github-access-draft"),!0}k(()=>{!r.repoId&&!r.pullRequestId&&!r.gistId&&a(T,0),r.repoId&&r.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=r.repoId,t.get("/api/repo/"+e.repoId).then(async D=>{e.githubConnection=D.data.connection||"oauth",e.sourceUrl="https://github.com/"+D.data.source.fullName,e._originalFullName=D.data.source.fullName,e.terms=D.data.options.terms.filter(V=>V).join(`
|
||||
`),e.source=D.data.source,e.role=D.data.role||"owner",e.coauthors=D.data.coauthors||[],e._originalBranch=D.data.source.branch,e.options=Object.assign({},e.options,D.data.options),e.conference=D.data.conference,e._originalConference=D.data.conference,e.repositoryID=D.data.source.repositoryID,e._originalRepositoryID=D.data.source.repositoryID,D.data.options.expirationDate&&(e.options.expirationDate=new Date(D.data.options.expirationDate)),!await T()&&(await Promise.all([N(),R()]),P())},()=>{n.url("/404")})),r.pullRequestId&&r.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=r.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async D=>{if(e.githubConnection=D.data.connection||"oauth",e.sourceUrl="https://github.com/"+D.data.source.repositoryFullName+"/pull/"+D.data.source.pullRequestId,e.terms=D.data.options.terms.filter(V=>V).join(`
|
||||
`),e.source=D.data.source,e.options=Object.assign({},e.options,D.data.options),e.conference=D.data.conference,e._originalConference=D.data.conference,D.data.options.expirationDate&&(e.options.expirationDate=new Date(D.data.options.expirationDate)),!await T())try{e.details=(await t.get(`/api/pr/${D.data.source.repositoryFullName}/${D.data.source.pullRequestId}`,{params:{connection:e.githubConnection}})).data}catch(V){let W=V&&V.data&&V.data.error;W&&(i("ERRORS."+W).then(oe=>{e.addToast({title:"Error",date:new Date,body:oe}),e.error=oe},console.error),De(W))}},()=>{n.url("/404")})),r.gistId&&r.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=r.gistId,t.get("/api/gist/"+e.gistId).then(async D=>{e.sourceUrl="https://gist.github.com/"+D.data.source.gistId,e.terms=D.data.options.terms.filter(V=>V).join(`
|
||||
`),e.source=D.data.source,e.options=Object.assign({},e.options,D.data.options),e.conference=D.data.conference,e._originalConference=D.data.conference,D.data.options.expirationDate&&(e.options.expirationDate=new Date(D.data.options.expirationDate)),!await T()&&await se()},()=>{n.url("/404")}))}),t.get("/github/connections").then(D=>{e.githubConnections=D.data,!r.repoId&&!r.pullRequestId&&!r.gistId&&e.githubConnection===void 0&&D.data.appEnabled&&D.data.appConnected&&(e.githubConnection="github-app")}).catch(()=>{}),e.urlSelected=async(D=!1)=>{e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,D||(e.terms=e.defaultTerms),!D&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],D||(e.source={type:"GitHubStream",branch:"",commit:""}),e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1;let V;try{V=parseGithubUrl(e.sourceUrl)}catch{p("sourceUrl","github",!1);return}p("sourceUrl","github",!0);try{V.gistId&&!V.repo?(e.detectedType="gist",e.source={gistId:V.gistId},await se()):V.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:V.owner+"/"+V.repo,pullRequestId:V.pullRequestId},await Z()):(e.detectedType="repo",await Promise.all([N(),R()]),P())}catch{return}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let D=e.branches.filter(W=>W.name==e.source.branch)[0];if(!D)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&!!e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=D.commit),e.readme=D.readme,await R(),P()}),e.getBranches=async D=>{let V=parseGithubUrl(e.sourceUrl);try{let W=await t.get(`/api/repo/${V.owner}/${V.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&g(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:D===!0||e.reconnectingSource?"1":"0",repositoryID:w()}});e.branches=W.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let oe=e.branches.filter(le=>le.name==e.source.branch);oe.length>0&&(!(!e.reconnectingSource&&!D&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=oe[0].commit),e.readme=oe[0].readme,await R(D))}catch(W){e.branches=[],e.sourceUnreachable=W&&(W.status===404||W.data&&W.data.error==="repo_not_found");let oe=W&&W.data&&W.data.error||(W&&W.status===404?"repo_not_found":"unknown_error");i("ERRORS."+oe).then(le=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:le}),e.error=le},console.error),typeof p=="function"&&p("sourceUrl","missing",!1)}};async function N(){let D=parseGithubUrl(e.sourceUrl);try{Oe();let V=await t.get(`/api/repo/${D.owner}/${D.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&g(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,repositoryID:w(),force:"1"}});e.details=V.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(V){throw V.data&&(i("ERRORS."+V.data.error).then(W=>{e.addToast({title:"Error",date:new Date,body:W}),e.error=W},console.error),De(V.data.error)),p("sourceUrl","missing",!1),V}}async function R(D){if(e.readme&&!D)return e.readme;let V=parseGithubUrl(e.sourceUrl);try{let W=await t.get(`/api/repo/${V.owner}/${V.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&g(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:D===!0?"1":"0",branch:e.source.branch,repositoryID:w()}});e.readme=W.data}catch{e.readme=""}}function I(){let D={terms:e.terms?e.terms.split(`
|
||||
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(D.branchName=e.source.branch);try{let V=parseGithubUrl(e.sourceUrl);D.repoName=`${V.owner}/${V.repo}`}catch{}return D}function C(D,V){let W=null,oe=0;return function(){W&&a.cancel(W),W=a(()=>{W=null;let U=++oe,te=D();te&&t.post("/api/anonymize-preview",te).then(L=>{U===oe&&V(L.data)},()=>{})},200)}}let A=C(()=>e.readme?{content:e.readme,options:I()}:null,D=>{e.anonymize_readme=D.content||"";let V="";try{let oe=parseGithubUrl(e.sourceUrl),le=e.source.branch||e.details&&e.details.defaultBranch||"main";V=`https://github.com/${oe.owner}/${oe.repo}/raw/${le}/`}catch{}let W=renderMD(e.anonymize_readme,V);e.html_readme=W,a(Prism.highlightAll,150)});function P(){!e.anonymize||!e.anonymize.terms||(e.termsRegexWarning=!!e.terms&&!!e.terms.match(/[-[\]{}()*+?.,\\^$|#]/g),A())}async function Z(){let D=parseGithubUrl(e.sourceUrl);try{Oe();let V=await t.get(`/api/pr/${D.owner}/${D.repo}/${D.pullRequestId}`,{params:{connection:e.githubConnection}});e.details=V.data,e.pullRequestId||(e.pullRequestId=D.repo+"-PR"+D.pullRequestId+"-"+generateRandomId(4))}catch(V){throw V.data&&(i("ERRORS."+V.data.error).then(W=>{e.addToast({title:"Error",date:new Date,body:W}),e.error=W},console.error),De(V.data.error)),p("sourceUrl","missing",!1),V}}let Y=He(new Map),j=new Set;function z(){let D=new Set,V=e.details&&e.details.pullRequest;if(!V)return D;typeof V.title=="string"&&D.add(V.title),typeof V.body=="string"&&D.add(V.body),typeof V.diff=="string"&&D.add(V.diff);let W=V.comments||[];for(let oe of W)typeof oe.author=="string"&&D.add(oe.author),typeof oe.body=="string"&&D.add(oe.body);return D}let ae=C(()=>{let D=z();j=D;let V=Array.from(D);return V.length===0?null:{contents:V,options:I()}},D=>{if(!D||!Array.isArray(D.contents))return;let V=Array.from(j),W=new Map;for(let oe=0;oe<V.length&&oe<D.contents.length;oe++)W.set(V[oe],D.contents[oe]);Y.clear(),W.forEach((oe,le)=>Y.set(le,oe))});e.anonymizePrContent=function(D){return D&&(Y.has(D)?Y.get(D):(j.has(D)||ae(),D))};async function se(){let D=parseGithubUrl(e.sourceUrl);try{if(Oe(),e.gistOAuthRequired=!1,e.githubConnections?.oauthConnected===!1){e.gistOAuthRequired=!0,p("sourceUrl","missing",!1);return}let V=await t.get(`/api/gist/source/${D.gistId}`);e.details=V.data,e.gistId||(e.gistId="gist-"+D.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(V){if(V.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,p("sourceUrl","missing",!1);return}throw V.data&&(i("ERRORS."+V.data.error).then(W=>{e.addToast({title:"Error",date:new Date,body:W}),e.error=W},console.error),De(V.data.error)),p("sourceUrl","missing",!1),V}}let re=He(new Map),ue=new Set;function _e(){let D=new Set,V=e.details&&e.details.gist;if(!V)return D;typeof V.description=="string"&&D.add(V.description),typeof V.ownerLogin=="string"&&D.add(V.ownerLogin);let W=V.files||[];for(let le of W)typeof le.filename=="string"&&D.add(le.filename),typeof le.content=="string"&&D.add(le.content);let oe=V.comments||[];for(let le of oe)typeof le.author=="string"&&D.add(le.author),typeof le.body=="string"&&D.add(le.body);return D}let he=C(()=>{let D=_e();ue=D;let V=Array.from(D);return V.length===0?null:{contents:V,options:I()}},D=>{if(!D||!Array.isArray(D.contents))return;let V=Array.from(ue),W=new Map;for(let oe=0;oe<V.length&&oe<D.contents.length;oe++)W.set(V[oe],D.contents[oe]);re.clear(),W.forEach((oe,le)=>re.set(le,oe)),me()});e.anonymizeGistContent=function(D){return D&&(re.has(D)?re.get(D):(ue.has(D)||he(),D))},e.previewGistFiles=[];function me(){let D=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=D.map(V=>({filename:e.anonymizeGistContent(V.filename),content:e.anonymizeGistContent(V.content),language:V.language}))}e.watch("details.gist.files",me,!0),e.watch("terms",me);function pe(){let D=e.conference;if(e.conference_data=null,!D)return;let V=e.isUpdate&&D===e._originalConference;t.get("/api/conferences/"+D).then(W=>{e.conference===D&&(e.conference_data=W.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!V&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.conference_data.options.image,e.options.pdf=e.conference_data.options.pdf,e.options.notebook=e.conference_data.options.notebook,e.options.link=e.conference_data.options.link))},()=>{e.conference===D&&(e.conference_data=null)})}function Oe(){p("repoId","used",!0),p("repoId","format",!0),p("pullRequestId","used",!0),p("pullRequestId","format",!0),p("gistId","used",!0),p("gistId","format",!0),p("sourceUrl","used",!0),p("sourceUrl","missing",!0),p("sourceUrl","access",!0),p("sourceUrl","github",!0),p("commit","exists",!0),p("conference","activated",!0),p("terms","format",!0),e.termsRegexWarning=!1}function Te(){let D=e.anonymize&&e.anonymize.expirationDate;ret
Line truncated
|
||||
`).filter(le=>le),connection:e.githubConnection,fullName:`${V.owner}/${V.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(W.options.pageSource=e.details.pageSource),Oe();let oe=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(oe,W,{headers:{"Content-Type":"application/json"}}).then(()=>{window.location.href="/status/"+e.repoId},le=>{le.data&&(i("ERRORS."+le.data.error).then(U=>{e.error=U},console.error),De(le.data.error))}).finally(()=>{D.target.disabled=!1})},e.anonymizeGist=D=>{if(Te())return;D.target.disabled=!0;let V=parseGithubUrl(e.sourceUrl),W={gistId:e.gistId,terms:e.terms.trim().split(`
|
||||
`).filter(le=>le),source:{gistId:V.gistId},options:e.options,conference:e.conference};Oe();let oe=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(oe,W,{headers:{"Content-Type":"application/json"}}).then(()=>{window.location.href="/gist/"+e.gistId},le=>{le.data&&(i("ERRORS."+le.data.error).then(U=>{e.error=U},console.error),De(le.data.error))}).finally(()=>{D.target.disabled=!1})},e.anonymizePullRequest=D=>{if(Te())return;D.target.disabled=!0;let V=parseGithubUrl(e.sourceUrl),W={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
|
||||
`).filter(le=>le),source:{repositoryFullName:`${V.owner}/${V.repo}`,pullRequestId:V.pullRequestId},options:e.options,conference:e.conference};Oe();let oe=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(oe,W,{headers:{"Content-Type":"application/json"}}).then(()=>{window.location.href="/pr/"+e.pullRequestId},le=>{le.data&&(i("ERRORS."+le.data.error).then(U=>{e.error=U},console.error),De(le.data.error))}).finally(()=>{D.target.disabled=!1})},e.watch("conference",()=>{pe()}),e.watch("terms",()=>{e.detectedType==="repo"&&P(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()}),e.watch("options.image",()=>{e.detectedType==="repo"&&P(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()}),e.watch("options.link",()=>{e.detectedType==="repo"&&P(),e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()}),e.watch("details",()=>{e.detectedType==="pr"&&ae(),e.detectedType==="gist"&&he()},!0)},Jo=function(e,t,o,r,n,i){let a=ht(),u=ls(),m=0,h=!1;e.on("dispose",()=>{h=!0,m++,v&&v.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"keydown",function(z){if((z.metaKey||z.ctrlKey)&&z.key==="k"){z.preventDefault();var ae=document.querySelector(".tree-search-input");ae&&(ae.focus(),ae.select())}});var v=null;e.onFileSearchChange=function(){v&&(v.resolve(),v=null);let z=e.fileSearchQuery;if(!z||z.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let ae=i.defer();v=ae,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(z)}`,{timeout:ae.promise}).then(function(se){if(!(h||v!==ae)){v=null,e.fileSearchLoading=!1;var re={};e.files.forEach(function(U){re[(U.path||"")+"/"+U.name]=!0});for(var ue=[],_e={},he=0;he<se.data.length;he++){var me=se.data[he];if(me.path)for(var pe=me.path.split("/"),Oe="",Te=0;Te<pe.length;Te++){var De=Oe;Oe=Oe?Oe+"/"+pe[Te]:pe[Te];var D=De+"/"+pe[Te];!re[D]&&!_e[D]&&(_e[D]=!0,ue.push({name:pe[Te],path:De}))}}ue.sort(function(U,te){return(U.path||"").split("/").length-(te.path||"").split("/").length}),ue.length>0&&e.files.push.apply(e.files,ue);for(var V=[],W=0;W<se.data.length;W++){var oe=se.data[W],le=(oe.path||"")+"/"+oe.name;!re[le]&&oe.size!=null&&(V.push(oe),re[le]=!0)}V.length>0&&e.files.push.apply(e.files,V),e.fileSearchResults=se.data}},function(){!h&&v===ae&&(v=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let k={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},p=["license","txt"],g=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],w=["wav","mp3","ogg","wma","flac","aac","m4a"],b=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(z,ae){if(e.repoId!=r.repoId)return j();if((r.path||"")!=e.filePath){e.filePath=r.path||"",e.paths=e.filePath.split("/").filter(se=>se&&se.trim().length>0),Y();for(let se=0;se<e.paths.length-1;se++){let re=se>0?e.paths.slice(0,se).join("/"):"";e.files.some(_e=>_e.path===re)||e.getFiles(re)}}});function T(){if(e.paths[0]!="")return;let z=["readme.md","readme.txt","readme.org","readme.1st","readme"],ae={};for(let re of e.files)re.name.toLowerCase().indexOf("readme")>-1&&(ae[re.name.toLowerCase()]=re.name);let se=null;for(let re of z)if(ae[re]){se=re;break}if(!se&&Object.keys(ae).length>0&&(se=Object.keys(ae)[0]),se){let re=o.url();re[re.length-1]!="/"&&(re+="/"),o.url(re+encodePathForUrl(ae[se]))}}e.fileCounts=null,e.getFiles=function(z){let ae=e.repoId;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(z)}&v=${e.options.lastUpdateDate}`).then(function(se){if(h||ae!==e.repoId)return[];let re=z||"";return e.files=e.files.filter(ue=>ue.path!==re),e.files.push(...se.data),se.data},function(se){if(h||ae!==e.repoId)return[];e.type="error",e.content=se&&se.data&&se.data.error||"unknown_error",e.files=[]})};function N(){let z=e.repoId;t.get(`/api/repo/${e.repoId}/files/counts`).then(function(ae){h||z!==e.repoId||(e.fileCounts=ae.data)},function(){e.fileCounts={}})}function R(){return e.files.filter(z=>z.name==e.paths[e.paths.length-1]&&z.path==e.paths.slice(0,e.paths.length-1).join("/"))[0]}var I=null;e.on("dispose",function(){I&&a.interval.cancel(I)});function C(z){if(h)return;let ae=e.repoId;t.get(`/api/repo/${e.repoId}/options`).then(se=>{if(!(h||ae!==e.repoId)){if(e.options=se.data,e.options.url){window.location=e.options.url;return}z&&z(se.data)}},se=>{if(!(h||ae!==e.repoId)){var re=se.data||{};if(re.error==="rate_limited"&&re.resetAt){let _e=function(){var he=Math.max(0,Math.ceil((e.rateLimitResetAt-Date.now())/1e3));if(he<=0)e.rateLimitCountdown="",e.rateLimitResetAt=0,I&&(a.interval.cancel(I),I=null),C(z);else{var me=Math.floor(he/60),pe=he%60;e.rateLimitCountdown=me>0?me+"m "+pe+"s":pe+"s"}};var ue=_e;e.type="rate_limited",e.rateLimitResetAt=re.resetAt,I&&a.interval.cancel(I),_e(),I=a.interval(_e,1e3)}else re.error==="re
Line truncated
|
||||
`+b.join(`
|
||||
`)],{type:"text/csv"}),N=document.createElement("a");N.href=URL.createObjectURL(T),N.download=r.username+"-repositories.csv",N.click()},e.showStatusMessage=p=>{let g=p.statusMessage||"(no message)";window.prompt(`Status message for ${p.repoId} (${p.status}):`,g)},e.fetchGithubInfo=p=>{let g=window.open("","_blank");g&&g.document.write("<pre>Loading GitHub info for "+p.repoId+"...</pre>"),t.get("/api/admin/repos/"+p.repoId+"/github").then(w=>{g&&(g.document.open(),g.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(w.data,null,2).replace(/[<>]/g,b=>b==="<"?"<":">")+"</pre>"),g.document.close())},w=>{let b=w&&w.data?JSON.stringify(w.data,null,2):String(w);g&&(g.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+b+"</pre>")})};function m(p){t.get("/api/admin/users/"+p+"/repos",{}).then(g=>{e.repositories=g.data},g=>{console.error(g)})}function h(p){t.get("/api/admin/users/"+p,{}).then(g=>{e.userInfo=g.data},g=>{console.error(g)})}h(r.username),m(r.username),e.banUser=()=>{confirm(`Ban user ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/ban`).then(()=>h(r.username),p=>console.error(p))},e.activateUser=()=>{t.post(`/api/admin/users/${r.username}/activate`).then(()=>h(r.username),p=>console.error(p))},e.promoteUser=()=>{confirm(`Promote ${r.username} to admin?`)&&t.post(`/api/admin/users/${r.username}/promote`).then(()=>h(r.username),p=>console.error(p))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/demote`).then(()=>h(r.username),p=>console.error(p))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function v(){t.get("/api/admin/tokens").then(p=>{e.tokens=p.data||[]},p=>{p.status!==401&&p.status!==403&&console.error(p)})}v(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(p=>{e.tokenForm.plaintext=p.data.token,e.tokenForm.name="",v()},p=>console.error(p))},e.revokeToken=p=>{confirm(`Revoke token "${p.name}"?`)&&t.delete("/api/admin/tokens/"+p.id).then(()=>v(),g=>console.error(g))},e.removeCache=p=>{confirm("Remove cached files for "+p.repoId+"?")&&t.delete("/api/admin/repos/"+p.repoId).then(()=>m(r.username),g=>console.error(g))},e.removeRepository=p=>{confirm("Remove repository "+p.repoId+"?")&&t.delete("/api/repo/"+p.repoId+"/").then(()=>m(r.username),g=>console.error(g))},e.updateRepository=p=>{let g=He({title:`Refreshing ${p.repoId}...`,date:new Date,body:`The repository ${p.repoId} is going to be refreshed.`});e.toasts.push(g),t.post(`/api/repo/${p.repoId}/refresh`).then(w=>{w.data.status=="ready"?g.title=`${p.repoId} is refreshed.`:g.title=`Refreshing of ${p.repoId}.`},w=>{g.title=`Error during the refresh of ${p.repoId}.`,g.body=w.body})},e.getGitHubRepositories=p=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(g=>{e.userInfo.repositories=g.data})};let k=null;e.watch("query",()=>{n.timeout.cancel(k),k=n.timeout(()=>{m(r.username)},500)},!0)},Ha=function(e,t,o){let r=ht(),n=ls();e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null&&o.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let i=w=>{if(w.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){w.preventDefault();let b=document.querySelector('.admin-filter-toolbar input[type="search"]');b&&b.focus()}};n(document,"keydown",i),e.on("dispose",()=>document.removeEventListener("keydown",i)),e.clearFilter=w=>{w==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[w]="",e.query.page=1},e.chips=[];let a=()=>{let w=[];(e.query.dateFrom||e.query.dateTo)&&w.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=w};e.statusCountFor=w=>{let b=(e.statusCounts||[]).find(T=>T._id===w);return b?b.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=w=>{e.query.sort===w?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=w,e.query.direction="desc"),e.query.page=1},e.sortIcon=w=>e.query.sort===w?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",m={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!1,expired:!1,removed:!1,error:!0,preparing:!0},h=loadFilterPrefs(u)||{};e.query=Object.assign({},m,h,{page:1,search:""});let v=o.search();v.search&&(e.query.search=v.search);let k="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(k)||"[]"),e.savePreset=()=>{let w=window.prompt("Preset name:");if(!w)return;let b=Object.assign({},e.query);delete b.page,e.presets=(e.presets||[]).filter(T=>T.name!==w),e.presets.push({name:w,query:b}),localStorage.setItem(k,JSON.stringify(e.presets))},e.applyPreset=w=>{Object.assign(e.query,w.query,{page:1})},e.deletePreset=w=>{e.presets=(e.presets||[]).filter(b=>b.name!==w.name),localStorage.setItem(k,JS
Line truncated
|
||||
`).map((he,me)=>me===0?he:_e+he).join(`
|
||||
`)}else typeof j=="number"||typeof j=="boolean"?ue=String(j):ue=JSON.stringify(j);Z.push(`${se}${ue}${re}`)}),Z.push("}"),Z.join(`
|
||||
`)}function h(N){let R=[],I="",C=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,A=0,P;for(;P=C.exec(N);)I+=N.slice(A,P.index),A=C.lastIndex,R.push({key:P[1],op:P[2]||"=",val:P[3]});return I+=N.slice(A),{filters:R,free:I.trim().toLowerCase()}}function v(N,R){for(let I of R.filters){let C=(P,Z,Y)=>{let j=parseFloat(P),z=parseFloat(Z);return Y==="="?String(P)===String(Z):Y==="!="?String(P)!==String(Z):Y===">="?j>=z:Y==="<="?j<=z:Y===">"?j>z:Y==="<"?j<z:!0},A;if(I.key==="code")A=N.displayMessage;else if(I.key==="module")A=N.module;else if(I.key==="status")A=N._status;else if(I.key==="url")A=N._url;else if(I.key==="repo")A=N._repoId;else if(I.key==="level")A=N.level;else continue;if(A==null||!C(A,I.val,I.op))return!1}return!(R.free&&((N.displayMessage||"")+" "+(N.module||"")+" "+(N._url||"")+" "+JSON.stringify(N.raw||[])).toLowerCase().indexOf(R.free)===-1)}function k(){let N=h(e.query.search||"");e.parsedFilterCount=N.filters.length;let R=e.query.bucket,I=e.entries.filter(A=>R&&A._bucket!==R?!1:v(A,N)),C=e.query.group;if(C){let A=Y=>C==="module"?Y.module:Y.displayMessage||Y.message||"_",P=new Map;for(let Y of I){let j=A(Y);if(P.has(j)){let z=P.get(j);z.count++,z._related.push(Y),new Date(Y.ts)>new Date(z.ts)&&(z.ts=Y.ts,z._url=Y._url,z._status=Y._status),new Date(Y.ts)<new Date(z._firstSeen)&&(z._firstSeen=Y.ts)}else{let z=Object.assign({},Y);z._key=`${C}:${j}`,z._related=[Y],z._firstSeen=Y.ts,z._lastHourCount=0,z.count=1,P.set(j,z)}}let Z=Date.now()-3600*1e3;for(let Y of P.values())Y._lastHourCount=Y._related.filter(j=>new Date(j.ts).getTime()>=Z).length;I=Array.from(P.values())}else I=I.map((A,P)=>(A._key="row:"+P+":"+A.ts,A._related=[A],A._firstSeen=A.ts,A._lastHourCount=0,A.count=1,A));e.query.sort==="count"?I.sort((A,P)=>P.count-A.count||new Date(P.ts)-new Date(A.ts)):I.sort((A,P)=>new Date(P.ts)-new Date(A.ts)),e.visible=I}function p(N){let R=N?e.entries.length:0,I=N?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:R,limit:I}}).then(C=>{let A=(C.data.entries||[]).map(u);e.entries=N?e.entries.concat(A):A,e.available=!!C.data.available,e.cap=C.data.max||e.cap,e.total=C.data.total||e.entries.length,k()},C=>console.error(C))}e.loadMore=()=>p(!0),e.canLoadMore=()=>e.entries.length<e.total;function g(){t.get("/api/admin/errors/stats").then(N=>{let R=N.data||{},I=R.prev24h?Math.round((R.last24h-R.prev24h)/R.prev24h*100):0;e.stats={last24h:R.last24h||0,prev24h:R.prev24h||0,delta:I,severity:R.severity||{error:0,warn:0,info:0},unique:R.unique||{error:0,warn:0,info:0},buckets:R.buckets||[],dropped:R.dropped||0}},N=>console.error(N))}function w(){p(),g()}e.barPx=(N,R)=>{let I=e.stats.buckets||[],C=0;for(let Y of I)C=Math.max(C,(Y.error||0)+(Y.warn||0)+(Y.info||0));if(!C)return 0;let A=(N.error||0)+(N.warn||0)+(N.info||0);if(!A)return 0;let P=Math.round(A/C*60),Z=N[R]||0;return Math.round(Z/A*P)},e.bucketTitle=N=>`${new Date(N.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${N.error||0} err \xB7 ${N.warn||0} warn \xB7 ${N.info||0} info`,e.toggle=N=>{e.expanded[N._key]=!e.expanded[N._key]},e.setBucket=N=>{e.query.bucket=N},e.refreshNow=w,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(w,N=>console.error(N))},e.exportCsv=()=>{let N=["ts","level","module","displayMessage","_status","_url","_repoId"],R=[N.join(",")];for(let P of e.visible)R.push(N.map(Z=>{let Y=P[Z]==null?"":String(P[Z]);return/[",\n]/.test(Y)?`"${Y.replace(/"/g,'""')}"`:Y}).join(","));let I=new Blob([R.join(`
|
||||
`)],{type:"text/csv;charset=utf-8"}),C=URL.createObjectURL(I),A=document.createElement("a");A.href=C,A.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(A),A.click(),document.body.removeChild(A),URL.revokeObjectURL(C)};function b(N){e.copyHint=`${N} copied`,n.timeout(()=>{e.copyHint=""},1500)}e.copyJson=N=>{navigator.clipboard.writeText(N._detailJson).then(()=>b("JSON"))},e.copyCurl=N=>{if(!N._url)return;let I=`curl -X ${N._method||"GET"} '${window.location.origin}${N._url}'`;navigator.clipboard.writeText(I).then(()=>b("curl"))},w();let T=r(()=>{e.query.autoRefresh&&w()},15e3);e.on("dispose",()=>r.cancel(T)),e.watch("query.search",k),e.watch("query.bucket",k),e.watch("query.sort",k),e.watch("query.group",k)},ja=function(e,t,o,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null){o.url("/");return}e.data=null,e.loading=!0,e.error=null;function n(p){if(p==null)return"\u2014";for(var g=["B","KB","MB","GB","TB"],w=0,b=p;b>=1024&&w<g.length-1;)b/=1024,w++;return b.toFixed(w>0?1:0)+" "+g[w]}e.humanBytes=n;function i(p){if(!p)return"\u2014";var g=Math.floor(p/86400),w=Math.floor(p%86400/3600),b=Math.floor(p%3600/60);return g>0?g+"d "+(w<10?"0":"")+w+"h":w>0?w+"h "+(b<10?"0":"")+b+"m":b+"m"}e.humanDuration=i;function a(p){return p==null?"\u2014":p>=1e6?(p/1e6).toFixed(1)+"M":p>=1e3?(p/1e3).toFixed(1)+"K":String(p)}e.humanNum=a,e.queueTotal=function(p){return p?(p.waiting||0)+(p.active||0)+(p.delayed||0)+(p.failed||0):0},e.statusCount=function(p){if(!e.data||!e.data.repos)return 0;for(var g=e.data.repos.statusBreakdown||[],w=0;w<g.length;w++)if(g[w]._id===p)return g[w].count;return 0},e.barPct=function(p){var g=e.data&&e.data.repos?e.data.repos.total:0;if(!g)return 0;var w=[p];p==="expired"&&w.push("expiring"),p==="removed"&&w.push("removing"),p==="preparing"&&w.push("download");var b=0;return w.forEach(function(T){b+=e.statusCount(T)}),Math.max(.4,b/g*100)},e.errPct=function(p){if(!e.data||!e.data.errors)return 0;var g=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[p]/g*100};function u(p){var g=p||[];return g.map(function(w,b){var T=g[b-1]||{},N=Object.assign({},w);return N.dailyRepositories=b?Math.max(0,(w.nbRepositories||0)-(T.nbRepositories||0)):0,N.dailyUsers=b?Math.max(0,(w.nbUsers||0)-(T.nbUsers||0)):0,N.dailyPageViews=b?Math.max(0,(w.nbPageViews||0)-(T.nbPageViews||0)):0,N})}function m(p){var g=p&&p.length?p[p.length-1]:{};return{repositories:g.dailyRepositories||0,users:g.dailyUsers||0,pageViews:g.dailyPageViews||0}}var h={};e.historyBarH=function(p,g){return!p||!h[g]?0:Math.max(1,Math.round(p[g]/h[g]*140))},e.historyLabel=function(p){if(!p||!p.date)return"";var g=new Date(p.date);return g.getUTCMonth()+1+"/"+g.getUTCDate()};function v(){t.get("/api/admin/overview").then(function(p){p.data.history=u(p.data.history),p.data.daily={today:m(p.data.history)},e.data=p.data,e.loading=!1,e.error=null,h={},(p.data.history||[]).forEach(function(g){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(w){(!h[w]||g[w]>h[w])&&(h[w]=g[w])})})},function(p){e.loading=!1,e.error=p.data&&p.data.error||"Failed to load overview"})}v();var k=r(v,3e4);e.on("dispose",function(){r.cancel(k)})};var Wa=[{path:"/connections",template:"partials/connections.htm",title:"GitHub connections \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>$a(e,t.http)},{path:"/signin",template:"partials/signin.htm",title:"Sign in \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ua(e,t.http)},{path:"/",template:"partials/home.htm",title:"Anonymous GitHub \u2013 Share the code, not the author",preserveExplorer:!1,setup:(e,t)=>Oa(e,t.http,t.location,t.window,t.timeout)},{path:"/dashboard",template:"partials/dashboard.htm",title:"Your anonymizations \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Aa(e,t.http,t.location,t.promises,t.window,t.quotaService)},{path:"/pr-dashboard",redirect:"/dashboard"},{path:"/anonymize/:repoId?",template:"partials/anonymize.htm",title:"New anonymization \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Jn(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/pull-request-anonymize/:pullRequestId?",template:"partials/anonymize.htm",title:"Anonymize a pull request \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Jn(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/gist-anonymize/:gistId?",template:"partials/anonymize.htm",title:"Anonymize a gist \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Jn(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/status/:repoId",template:"partials/status.htm",title:"Repository status \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ia(e,t.http,t.params)},{path:"/conferences",template:"partials/conferences.htm",title:"Your conferences \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>qa(e,t.http,t.location)
Line truncated
|
||||
anonymous_github`)])])])])]),f(" Limitations "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-exclamation-triangle mr-2"}),y("Limitations ")]),s("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[s("h3",{class:"panel-title"},[s("a",{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? ")])]),s("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[s("div",{class:"panel-body p-3"},[s("ul",null,[s("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),s("li",null," Files larger than 8 MB are not supported. "),s("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),s("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),f(" Privacy & Security "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-shield-alt mr-2"}),y("Privacy & Security ")]),s("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[s("h3",{class:"panel-title"},[s("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),s("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[s("div",{class:"panel-body p-3"},[s("p",null," Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the anonymization service. When a repository is removed or expires, only its configuration is retained \u2014 this makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[s("h3",{class:"panel-title"},[s("a",{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? ")])]),s("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[s("div",{class:"panel-body p-3"},[s("p",null," Anonymous GitHub only reads your repositories \u2014 it never pushes commits, opens issues, modifies settings, or deletes anything. From your perspective as a user, the service is read-only. "),s("p",null,[y(" However, GitHub's OAuth scopes do not offer a read-only option for private repositories: the only scope that grants access to private repos is "),s("code",null,"repo"),y(", which is documented as full read/write access. To support users who want to anonymize a private repository, Anonymous GitHub must request that scope, and GitHub then displays the broader permission text at sign-in. The application itself only ever performs read operations against the GitHub API. ")]),s("p",null,[y(" If you only anonymize public repositories, the source code is open and can be audited on the "),s("a",{href:"https://github.com/tdurieux/anonymous_github/",target:"_blank"},"GitHub repository"),y(", or you can self-host your own instance. ")])])])]),s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[s("h3",{class:"panel-title"},[s("a",{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? ")])]),s("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[s("div",{class:"panel-body p-3"},[s("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),f(" Self-Hosting "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-server mr-2"}),y("Self-Hosting ")]),s("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[s("h3",{class:"panel-title"},[s("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-ho
Line truncated
|
||||
`+JSON.stringify(t):""}`},[je.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${Ac(t)}" via a navigation guard.`},[je.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[je.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[je.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function ao(e,t){return Oe(new Error,{type:e,[fa]:!0},t)}function Mt(e,t){return e instanceof Error&&fa in e&&(t==null||!!(e.type&t))}var Tc=["params","query","hash"];function Ac(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let n of Tc)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function ma(e){let t={};if(e===""||e==="?")return t;let n=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<n.length;++r){let s=n[r].replace(ea," "),i=s.indexOf("="),a=Do(i<0?s:s.slice(0,i)),u=i<0?null:Do(s.slice(i+1));if(a in t){let m=t[a];wt(m)||(m=t[a]=[m]),m.push(u)}else t[a]=u}return t}function Qn(e){let t="";for(let n in e){let r=e[n];if(n=wc(n),r==null){r!==void 0&&(t+=(t.length?"&":"")+n);continue}(wt(r)?r.map(s=>s&&Hn(s)):[r&&Hn(r)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+n,s!=null&&(t+="="+s))})}return t}function ha(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=wt(r)?r.map(s=>s==null?null:""+s):r==null?r:""+r)}return t}var va=Symbol(""),Xn=Symbol(""),Qs=Symbol(""),Xs=Symbol(""),Zs=Symbol("");function To(){let e=[];function t(r){return e.push(r),()=>{let s=e.indexOf(r);s>-1&&e.splice(s,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function Gt(e,t,n,r,s,i=a=>a()){let a=r&&(r.enterCallbacks[s]=r.enterCallbacks[s]||[]);return()=>new Promise((u,m)=>{let h=f=>{f===!1?m(ao(je.NAVIGATION_ABORTED,{from:n,to:t})):f instanceof Error?m(f):Rc(f)?m(ao(je.NAVIGATION_GUARD_REDIRECT,{from:t,to:f})):(a&&r.enterCallbacks[s]===a&&typeof f=="function"&&a.push(f),u())},v=i(()=>e.call(r&&r.instances[s],t,n,h)),w=Promise.resolve(v);e.length<3&&(w=w.then(h)),w.catch(f=>m(f))})}function xs(e,t,n,r,s=i=>i()){let i=[];for(let a of e)for(let u in a.components){let m=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(Zi(m)){let h=(m.__vccOpts||m)[t];h&&i.push(Gt(h,n,r,a,u,s))}else{let h=m();i.push(()=>h.then(v=>{if(!v)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let w=uc(v)?v.default:v;a.mods[u]=v,a.components[u]=w;let f=(w.__vccOpts||w)[t];return f&&Gt(f,n,r,a,u,s)()}))}}return i}function ga(e,t){let n=[],r=[],s=[],i=Math.max(t.matched.length,e.matched.length);for(let a=0;a<i;a++){let u=t.matched[a];u&&(e.matched.find(h=>io(h,u))?r.push(u):n.push(u));let m=e.matched[a];m&&(t.matched.find(h=>io(h,m))||s.push(m))}return[n,r,s]}var Oc=()=>location.protocol+"//"+location.host;function Ra(e,t){let{pathname:n,search:r,hash:s}=t,i=e.indexOf("#");if(i>-1){let a=s.includes(e.slice(i))?e.slice(i).length:1,u=s.slice(a);return u[0]!=="/"&&(u="/"+u),Wn(u,"")}return Wn(n,e)+r+s}function Ic(e,t,n,r){let s=[],i=[],a=null,u=({state:f})=>{let b=Ra(e,location),C=n.value,k=t.value,R=0;if(f){if(n.value=b,t.value=f,a&&a===C){a=null;return}R=k?f.position-k.position:0}else r(b);s.forEach(D=>{D(n.value,C,{delta:R,type:Ys.pop,direction:R?R>0?Js.forward:Js.back:Js.unknown})})};function m(){a=n.value}function h(f){s.push(f);let b=()=>{let C=s.indexOf(f);C>-1&&s.splice(C,1)};return i.push(b),b}function v(){if(document.visibilityState==="hidden"){let{history:f}=window;if(!f.state)return;f.replaceState(Oe({},f.state,{scroll:ds()}),"")}}function w(){for(let f of i)f();i=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",v),document.removeEventListener("visibilitychange",v)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",v),document.addEventListener("visibilitychange",v),{pauseListeners:m,listen:h,destroy:w}}function ya(e,t,n,r=!1,s=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:s?ds():null}}function Pc(e){let{history:t,location:n}=window,r={value:Ra(e,n)},s={value:t.state};s.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(m,h,v){let w=e.indexOf("#"),f=w>-1?(n.host&&document.querySelector("base")?e:e.slice(w))+m:Oc()+e+m;try{t[v?"replaceState":"pushState"](h,"",f),s.value=h}catch(b){console.error(b),n[v?"replace":"assign"](f)}}function a(m,h){i(m,Oe({},t.state,ya(s.value.back,m,s.value.forward,!0),h,{position:s.value.position}),!0),r.value=m}function u(m,h){let v=Oe({},s.value,t.state,{forward:m,scroll:ds()});i(v.current,v,!0),i(m,Oe({},ya(r.value,m,null),{position:v.position+1},h),!1),r.value=m}return{location:r,state:s,push:u,replace:a}}function Ta(e){e=la(e);let t=Pc(e),n=Ic(e,t.state,t.location,t.replace);function r(i,a=!0){a||n.pauseListeners(),history.go(i)}let s=Oe({locatLine truncated
|
||||
`)),e.options=Object.assign({},e.options,h.options),i.restore()})}a();let u=null;e.saveDefault=m=>{if(m&&m.preventDefault&&m.preventDefault(),e.saving)return;let h=i.checkpoint(),v={terms:e.terms.split(`
|
||||
`).map(w=>w.trim()).filter(w=>w.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",v).then(()=>{i.accept(h),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",u&&r.cancel(u),u=r(()=>{e.message=null},2500)},w=>{e.saving=!1;let b="ERRORS."+(w&&w.data&&w.data.error);n(b).then(C=>{e.error=C&&C!==b?C:"Unable to save your defaults. Please try again."},()=>{e.error="Unable to save your defaults. Please try again."})})},e.deleteAccount=()=>{confirm("Delete your account? All your anonymized repositories, gists, and pull requests will be removed, and your personal data will be erased. This cannot be undone.")&&(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},La=function(e,t,n){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(r=>{n.url("/dashboard")},r=>{e.error=r.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Ua=function(e,t,n,r,s){e.user&&!e.user.status&&n.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&n.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,m){let h={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(h===void 0)return;u.preventDefault();let v=e.features.length,w=h==="first"?0:h==="last"?v-1:(m+h+v)%v;e.feature=e.features[w].key,s(()=>{let f=r.document.getElementById("feature-tab-"+e.feature);f&&f.focus()})},e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function i(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests})}i();function a(u){let m={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return m;let h=new Array(u.length-1);for(let k=1;k<u.length;k++)h[k-1]=u[k]-u[k-1];let v=h.length,w=Math.max.apply(null,h),f=Math.min.apply(null,h),b=Math.min(0,f),C=w-b||1;m.viewW=v*2,m.bars=new Array(v);for(let k=0;k<v;k++){let R=(h[k]-b)/C,D=Math.max(1.5,R*34);m.bars[k]={x:(k*2+.25).toFixed(2),y:(36-D).toFixed(2),w:"1.5",h:D.toFixed(2)}}if(m.deltaToday=h[v-1],v>=2){let k=h[v-2];k&&(m.pctChange=(m.deltaToday-k)/k*100)}return m.pctAbs=Math.round(Math.abs(m.pctChange)),m.isUp=m.pctChange>=0,m}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let m=u.data||[];e.history={repositories:a(m.map(h=>h.nbRepositories||0)),users:a(m.map(h=>h.nbUsers||0)),pageViews:a(m.map(h=>h.nbPageViews||0)),pullRequests:a(m.map(h=>h.nbPullRequests||0))}})},Fa=function(e,t,n,r,s,i){let a=ct();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&n.url("/")}),e.user==null&&n.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,rt(()=>document.querySelector(".dashboard-quota > summary")?.focus())},Kt()(document,"click",_=>{e.quotaExpanded&&!_.target.closest?.(".dashboard-quota")&&
Line truncated
|
||||
`)),e.options=Object.assign({},e.options,M.options),e.options.expirationDate=M.options&&M.options.expirationDate?new Date(M.options.expirationDate):T(),!r.repoId&&!r.pullRequestId&&!r.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),E&&E()})}function I(E,P,M){e.anonymize&&e.anonymize[E]&&e.anonymize[E].setValidity(P,M)}function J(E){try{let P=parseGithubUrl(E);if(P&&P.owner&&P.repo)return P.owner+"/"+P.repo}catch{}return null}e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await ae()}catch{}}};function Y(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:J(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function K(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function B(){let E;try{E=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!E||E.path!==s.path()||Date.now()-E.savedAt>=30*6e4)return R.restore()?(e.sourceUrl&&await K(),!0):!1;R.restore();for(let P of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(E.draft,P)&&(e[P]=E.draft[P]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await K(),sessionStorage.removeItem("github-access-draft"),!0}O(()=>{!r.repoId&&!r.pullRequestId&&!r.gistId&&a(B,0),r.repoId&&r.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=r.repoId,t.get("/api/repo/"+e.repoId).then(async E=>{e.githubConnection=E.data.connection||"oauth",e.sourceUrl="https://github.com/"+E.data.source.fullName,e._originalFullName=E.data.source.fullName,e.terms=E.data.options.terms.filter(P=>P).join(`
|
||||
`),e.source=E.data.source,e.role=E.data.role||"owner",e.coauthors=E.data.coauthors||[],e._originalBranch=E.data.source.branch,e.options=Object.assign({},e.options,E.data.options),e.conference=E.data.conference,e._originalConference=E.data.conference,e.repositoryID=E.data.source.repositoryID,e._originalRepositoryID=E.data.source.repositoryID,E.data.options.expirationDate&&(e.options.expirationDate=new Date(E.data.options.expirationDate)),C=k(parseGithubUrl(e.sourceUrl)),!await B()&&(await Promise.all([ae(),oe()]),ve())},()=>{s.url("/404")})),r.pullRequestId&&r.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=r.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async E=>{if(e.githubConnection=E.data.connection||"oauth",e.sourceUrl="https://github.com/"+E.data.source.repositoryFullName+"/pull/"+E.data.source.pullRequestId,e.terms=E.data.options.terms.filter(P=>P).join(`
|
||||
`),e.source=E.data.source,e.options=Object.assign({},e.options,E.data.options),e.conference=E.data.conference,e._originalConference=E.data.conference,E.data.options.expirationDate&&(e.options.expirationDate=new Date(E.data.options.expirationDate)),C=k(parseGithubUrl(e.sourceUrl)),!await B())try{await me()}catch{}},()=>{s.url("/404")})),r.gistId&&r.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=r.gistId,t.get("/api/gist/"+e.gistId).then(async E=>{e.sourceUrl="https://gist.github.com/"+E.data.source.gistId,e.terms=E.data.options.terms.filter(P=>P).join(`
|
||||
`),e.source=E.data.source,e.options=Object.assign({},e.options,E.data.options),e.conference=E.data.conference,e._originalConference=E.data.conference,E.data.options.expirationDate&&(e.options.expirationDate=new Date(E.data.options.expirationDate)),C=k(parseGithubUrl(e.sourceUrl)),!await B()&&await Se()},()=>{s.url("/404")}))}),t.get("/github/connections").then(E=>{e.githubConnections=E.data,!r.repoId&&!r.pullRequestId&&!r.gistId&&e.githubConnection===void 0&&E.data.appEnabled&&E.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&K())}).catch(()=>{}),e.urlSelected=async(E=!1)=>{let P=++m,M;try{M=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,he.cancel(),Ee.cancel(),Ve.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",I("sourceUrl","github",!1);return}let z=k(M),y=E||C===z;C=z,e.sourceUrlError="",I("sourceUrl","github",!0);let S=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,S&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!y&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],y||(e.source={type:"GitHubStream",branch:"",commit:""}),he.cancel(),Ee.cancel(),Ve.cancel(),ie.clear(),Ce.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e._preservingDraft=y;try{M.gistId&&!M.repo?(e.detectedType="gist",e.source={gistId:M.gistId},await Se()):M.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:M.owner+"/"+M.repo,pullRequestId:M.pullRequestId},await me()):(e.detectedType="repo",await Promise.all([ae(),oe()]),ve())}catch{}finally{P===m&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let E=e.branches.filter(M=>M.name==e.source.branch)[0];if(!E)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&!!e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=E.commit),e.readme=E.readme,await oe(),ve()}),e.getBranches=async E=>{let P=parseGithubUrl(e.sourceUrl),M=f();try{let z=await t.get(`/api/repo/${P.owner}/${P.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&J(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:E===!0||e.reconnectingSource?"1":"0",repositoryID:Y()}});if(!w(M))return;e.branches=z.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let y=e.branches.filter(S=>S.name==e.source.branch);y.length>0&&(!(!e.reconnectingSource&&!E&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=y[0].commit),e.readme=y[0].readme,await oe(E))}catch(z){if(!w(M))return;e.branches=[],e.sourceUnreachable=z&&(z.status===404||z.data&&z.data.error==="repo_not_found");let y=z&&z.data&&z.data.error||(z&&z.status===404?"repo_not_found":"unknown_error");i("ERRORS."+y).then(S=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:S}),e.error=S},console.error),typeof I=="function"&&I("sourceUrl","missing",!1)}finally{b(M)}};async function ae(){let E=parseGithubUrl(e.sourceUrl),P=f();try{U();let M=await t.get(`/api/repo/${E.owner}/${E.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&J(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,repositoryID:Y(),force:"1"}});if(!w(P))return;e.details=M.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(M){if(!w(P))return;throw M.data&&(i("ERRORS."+M.data.error).then(z=>{e.addToast({title:"Error",date:new Date,body:z}),e.error=z},console.error),F(M.data.error)),I("sourceUrl","missing",!1),M}finally{b(P)}}async function oe(E){if(e.readme&&!E)return e.readme;let P=parseGithubUrl(e.sourceUrl),M=f();try{let z=await t.get(`/api/repo/${P.owner}/${P.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&J(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:E===!0?"1":"0",branch:e.source.branch,repositoryID:Y()}});w(M,!0)&&(e.readme=z.data)}catch{w(M,!0)&&(e.readme="")}finally{b(M)}}function ne(){let E={terms:e.terms?e.terms.split(`
|
||||
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(E.branchName=e.source.branch);try{let P=parseGithubUrl(e.sourceUrl);E.repoName=`${P.owner}/${P.repo}`}catch{}return E}function ue(E,P,M){let z=null,y=0,S=()=>{if(e.detectedType!==E)return;z&&a.cancel(z);let q=++y;e.previewStatus="loading",z=a(()=>{z=null;let H=P();if(!H){e.previewStatus="idle";return}t.post("/api/anonymize-preview",H).then(j=>{q===y&&(M(j.data,H),e.previewStatus="ready")}).catch(()=>{q===y&&(e.previewStatus="error")})},200)};return S.cancel=()=>{y++,z&&a.cancel(z),z=null},e.on("dispose",S.cancel),S}e.retryPreview=()=>{e.detectedType==="repo"&&he(),e.detectedType==="pr"&&Ee(),e.detectedType==="gist"&&Ve()};let he=ue("repo",()=>e.readme?{content:e.readme,options:ne()}:null,E=>{if(typeof E?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=E.content;let P="";try{let z=parseGithubUrl(e.sourceUrl),y=e.source.branch||e.details&&e.details.defaultBranch||"main";P=`https://github.com/${z.owner}/${z.repo}/raw/${y}/`}catch{}let M=renderMD(e.anonymize_readme,P);e.html_readme=M,a(Prism.highlightAll,150)});function ve(){!e.anonymize||!e.anonymize.terms||(e.termsRegexWarning=!!e.terms&&!!e.terms.match(/[-[\]{}()*+?.,\\^$|#]/g),he())}async function me(){let E=parseGithubUrl(e.sourceUrl),P=f();try{U();let M=await t.get(`/api/pr/${E.owner}/${E.repo}/${E.pullRequestId}`,{params:{connection:e.githubConnection}});if(!w(P))return;e.details=M.data,e.pullRequestId||(e.pullRequestId=E.repo+"-PR"+E.pullRequestId+"-"+generateRandomId(4))}catch(M){if(!w(P))return;throw M.data&&(i("ERRORS."+M.data.error).then(z=>{e.addToast({title:"Error",date:new Date,body:z}),e.error=z},console.error),F(M.data.error)),I("sourceUrl","missing",!1),M}finally{b(P)}}let ie=He(new Map);function Pe(){let E=new Set,P=e.details&&e.details.pullRequest;if(!P)return E;typeof P.title=="string"&&E.add(P.title),typeof P.body=="string"&&E.add(P.body),typeof P.diff=="string"&&E.add(P.diff);let M=P.comments||[];for(let z of M)typeof z.author=="string"&&E.add(z.author),typeof z.body=="string"&&E.add(z.body);return E}let Ee=ue("pr",()=>{let E=Pe(),P=Array.from(E);return P.length===0?null:{contents:P,options:ne()}},(E,P)=>{if(!Array.isArray(E?.contents)||E.contents.length!==P.contents.length||E.contents.some(y=>typeof y!="string"))throw Error("Invalid preview response");let M=P.contents,z=new Map;for(let y=0;y<M.length&&y<E.contents.length;y++)z.set(M[y],E.contents[y]);ie.clear(),z.forEach((y,S)=>ie.set(S,y))});e.anonymizePrContent=function(E){return E&&(ie.has(E)?ie.get(E):"")};async function Se(){let E=parseGithubUrl(e.sourceUrl),P=f();try{if(U(),e.gistOAuthRequired=!1,e.githubConnections?.oauthConnected===!1){e.gistOAuthRequired=!0,I("sourceUrl","missing",!1);return}let M=await t.get(`/api/gist/source/${E.gistId}`);if(!w(P))return;e.details=M.data,e.gistId||(e.gistId="gist-"+E.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(M){if(!w(P))return;if(M.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,I("sourceUrl","missing",!1);return}throw M.data&&(i("ERRORS."+M.data.error).then(z=>{e.addToast({title:"Error",date:new Date,body:z}),e.error=z},console.error),F(M.data.error)),I("sourceUrl","missing",!1),M}finally{b(P)}}let Ce=He(new Map);function Te(){let E=new Set,P=e.details&&e.details.gist;if(!P)return E;typeof P.description=="string"&&E.add(P.description),typeof P.ownerLogin=="string"&&E.add(P.ownerLogin);let M=P.files||[];for(let y of M)typeof y.filename=="string"&&E.add(y.filename),typeof y.content=="string"&&E.add(y.content);let z=P.comments||[];for(let y of z)typeof y.author=="string"&&E.add(y.author),typeof y.body=="string"&&E.add(y.body);return E}let Ve=ue("gist",()=>{let E=Te(),P=Array.from(E);return P.length===0?null:{contents:P,options:ne()}},(E,P)=>{if(!Array.isArray(E?.contents)||E.contents.length!==P.contents.length||E.contents.some(y=>typeof y!="string"))throw Error("Invalid preview response");let M=P.contents,z=new Map;for(let y=0;y<M.length&&y<E.contents.length;y++)z.set(M[y],E.contents[y]);Ce.clear(),z.forEach((y,S)=>Ce.set(S,y)),Ue()});e.anonymizeGistContent=function(E){return E&&(Ce.has(E)?Ce.get(E):"")},e.previewGistFiles=[];function Ue(){let E=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=E.map(P=>({filename:e.anonymizeGistContent(P.filename),content:e.anonymizeGistContent(P.content),language:P.language}))}e.watch("details.gist.files",Ue,!0),e.watch("terms",Ue);function Fe(){let E=e.conference;if(e.conference_data=null,!E)return;let P=e.isUpdate&&E===e._originalConference;t.get("/api/conferences/"+E).then(M=>{e.conference===E&&(e.conference_data=M.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!P&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.conference_data.options.ima
Line truncated
|
||||
`).filter(S=>S),connection:e.githubConnection,fullName:`${M.owner}/${M.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(z.options.pageSource=e.details.pageSource),U();let y=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(y,z,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/status/"+e.repoId},S=>{S.data&&(i("ERRORS."+S.data.error).then(q=>{e.error=q},console.error),F(S.data.error))}).finally(()=>{P.disabled=!1})},e.anonymizeGist=E=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(te())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let P=E.currentTarget||E.target.closest?.("button")||E.target;P.disabled=!0;let M=parseGithubUrl(e.sourceUrl),z={gistId:e.gistId,terms:e.terms.trim().split(`
|
||||
`).filter(S=>S),source:{gistId:M.gistId},options:e.options,conference:e.conference};U();let y=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(y,z,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/gist/"+e.gistId},S=>{S.data&&(i("ERRORS."+S.data.error).then(q=>{e.error=q},console.error),F(S.data.error))}).finally(()=>{P.disabled=!1})},e.anonymizePullRequest=E=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(te())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let P=E.currentTarget||E.target.closest?.("button")||E.target;P.disabled=!0;let M=parseGithubUrl(e.sourceUrl),z={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
|
||||
`).filter(S=>S),source:{repositoryFullName:`${M.owner}/${M.repo}`,pullRequestId:M.pullRequestId},options:e.options,conference:e.conference};U();let y=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(y,z,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/pr/"+e.pullRequestId},S=>{S.data&&(i("ERRORS."+S.data.error).then(q=>{e.error=q},console.error),F(S.data.error))}).finally(()=>{P.disabled=!1})},e.watch("conference",()=>{Fe()}),e.watch("terms",()=>{e.detectedType==="repo"&&ve(),e.detectedType==="pr"&&Ee(),e.detectedType==="gist"&&Ve()}),e.watch("options.image",()=>{e.detectedType==="repo"&&ve(),e.detectedType==="pr"&&Ee(),e.detectedType==="gist"&&Ve()}),e.watch("options.link",()=>{e.detectedType==="repo"&&ve(),e.detectedType==="pr"&&Ee(),e.detectedType==="gist"&&Ve()}),e.watch("details",()=>{e.detectedType==="pr"&&Ee(),e.detectedType==="gist"&&Ve()},!0)},tr=function(e,t,n,r,s,i){let a=ct(),u=Kt(),m=0,h=!1;e.on("dispose",()=>{h=!0,m++,v&&v.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"keydown",function(B){if((B.metaKey||B.ctrlKey)&&B.key==="k"){B.preventDefault();var ae=document.querySelector(".tree-search-input");ae&&(ae.focus(),ae.select())}});var v=null;e.onFileSearchChange=function(){v&&(v.resolve(),v=null);let B=e.fileSearchQuery;if(!B||B.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let ae=i.defer();v=ae,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(B)}`,{timeout:ae.promise}).then(function(oe){if(!(h||v!==ae)){v=null,e.fileSearchLoading=!1;var ne={};e.files.forEach(function(U){ne[(U.path||"")+"/"+U.name]=!0});for(var ue=[],he={},ve=0;ve<oe.data.length;ve++){var me=oe.data[ve];if(me.path)for(var ie=me.path.split("/"),Pe="",Ee=0;Ee<ie.length;Ee++){var Se=Pe;Pe=Pe?Pe+"/"+ie[Ee]:ie[Ee];var Ce=Se+"/"+ie[Ee];!ne[Ce]&&!he[Ce]&&(he[Ce]=!0,ue.push({name:ie[Ee],path:Se}))}}ue.sort(function(U,te){return(U.path||"").split("/").length-(te.path||"").split("/").length}),ue.length>0&&e.files.push.apply(e.files,ue);for(var Te=[],Ve=0;Ve<oe.data.length;Ve++){var Ue=oe.data[Ve],Fe=(Ue.path||"")+"/"+Ue.name;!ne[Fe]&&Ue.size!=null&&(Te.push(Ue),ne[Fe]=!0)}Te.length>0&&e.files.push.apply(e.files,Te),e.fileSearchResults=oe.data}},function(){!h&&v===ae&&(v=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let w={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},f=["license","txt"],b=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],C=["wav","mp3","ogg","wma","flac","aac","m4a"],k=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(B,ae){if(e.repoId!=r.repoId)return K();if((r.path||"")!=e.filePath){e.filePath=r.path||"",e.paths=e.filePath.split("/").filter(oe=>oe&&oe.trim().length>0),Y();for(let oe=0;oe<e.paths.length-1;oe++){let ne=oe>0?e.paths.slice(0,oe).join("/"):"";e.files.some(he=>he.path===ne)||e.getFiles(ne)}}});function R(){if(e.paths[0]!="")return;let B=["readme.md","readme.txt","readme.org","readme.1st","readme"],ae={};for(let ne of e.files)ne.name.toLowerCase().indexOf("readme")>-1&&(ae[ne.name.toLowerCase()]=ne.name);let oe=null;for(let ne of B)if(ae[ne]){oe=ne;break}if(!oe&&Object.keys(ae).length>0&&(oe=Object.keys(ae)[0]),oe){let ne=n.url();ne[ne.length-1]!="/"&&(ne+="/"),n.url(ne+encodePathForUrl(ae[oe]))}}e.fileCounts=null,e.getFiles=function(B){let ae=e.repoId;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(B)}&v=${e.options.lastUpdateDate}`).then(function(oe){if(h||ae!==e.repoId)return[];let ne=B||"";return e.files=e.files.filter(ue=>ue.path!==ne),e.files.push(...oe.data),oe.data},function(oe){if(h||ae!==e.repoId)return[];e.type="error",e.content=oe&&oe.data&&oe.data.error||"unknown_error",e.files=[]})};function D(){let B=e.repoId;t.get(`/api/repo/${e.repoId}/files/counts`).then(function(ae){h||B!==e.repoId||(e.fileCounts=ae.data)},function(){e.fileCounts={}})}function T(){return e.files.filter(B=>B.name==e.paths[e.paths.length-1]&&B.path==e.paths.slice(0,e.paths.length-1).join("/"))[0]}var V=null;e.on("dispose",function(){V&&a.interval.cancel(V)});function _(B){if(h)return;let ae=e.repoId;t.get(`/api/repo/${e.repoId}/options`).then(oe=>{if(!(h||ae!==e.repoId)){if(e.options=oe.data,e.options.url){window.location=e.options.url;return}B&&B(oe.data)}},oe=>{if(!(h||ae!==e.repoId)){var ne=oe.data||{};if(ne.error==="rate_limited"&&ne.resetAt){let he=function(){var ve=Math.max(0,Math.ceil((e.rateLimitResetAt-Date.now())/1e3));if(ve<=0)e.rateLimitCountdown="",e.rateLimitResetAt=0,V&&(a.interval.cancel(V),V=null),_(B);else{var me=Math.floor(ve/60),ie=ve%60;e.rateLimitCountdown=me>0?me+"m "+ie+"s":ie+"s"}};var ue=he;e.type="rate_limited",e.rateLimitResetAt=ne.resetAt,V&&a.interval.cancel(V),he(),V=a.interval(he,1e3)}else ne.err
Line truncated
|
||||
`+k.join(`
|
||||
`)],{type:"text/csv"}),D=document.createElement("a");D.href=URL.createObjectURL(R),D.download=r.username+"-repositories.csv",D.click()},e.showStatusMessage=f=>{let b=f.statusMessage||"(no message)";window.prompt(`Status message for ${f.repoId} (${f.status}):`,b)},e.fetchGithubInfo=f=>{let b=window.open("","_blank");b&&b.document.write("<pre>Loading GitHub info for "+f.repoId+"...</pre>"),t.get("/api/admin/repos/"+f.repoId+"/github").then(C=>{b&&(b.document.open(),b.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(C.data,null,2).replace(/[<>]/g,k=>k==="<"?"<":">")+"</pre>"),b.document.close())},C=>{let k=C&&C.data?JSON.stringify(C.data,null,2):String(C);b&&(b.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+k+"</pre>")})};function m(f){t.get("/api/admin/users/"+f+"/repos",{}).then(b=>{e.repositories=b.data},b=>{console.error(b)})}function h(f){t.get("/api/admin/users/"+f,{}).then(b=>{e.userInfo=b.data},b=>{console.error(b)})}h(r.username),m(r.username),e.banUser=()=>{confirm(`Ban user ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/ban`).then(()=>h(r.username),f=>console.error(f))},e.activateUser=()=>{t.post(`/api/admin/users/${r.username}/activate`).then(()=>h(r.username),f=>console.error(f))},e.promoteUser=()=>{confirm(`Promote ${r.username} to admin?`)&&t.post(`/api/admin/users/${r.username}/promote`).then(()=>h(r.username),f=>console.error(f))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/demote`).then(()=>h(r.username),f=>console.error(f))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function v(){t.get("/api/admin/tokens").then(f=>{e.tokens=f.data||[]},f=>{f.status!==401&&f.status!==403&&console.error(f)})}v(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(f=>{e.tokenForm.plaintext=f.data.token,e.tokenForm.name="",v()},f=>console.error(f))},e.revokeToken=f=>{confirm(`Revoke token "${f.name}"?`)&&t.delete("/api/admin/tokens/"+f.id).then(()=>v(),b=>console.error(b))},e.removeCache=f=>{confirm("Remove cached files for "+f.repoId+"?")&&t.delete("/api/admin/repos/"+f.repoId).then(()=>m(r.username),b=>console.error(b))},e.removeRepository=f=>{confirm("Remove repository "+f.repoId+"?")&&t.delete("/api/repo/"+f.repoId+"/").then(()=>m(r.username),b=>console.error(b))},e.updateRepository=f=>{let b=He({title:`Refreshing ${f.repoId}...`,date:new Date,body:`The repository ${f.repoId} is going to be refreshed.`});e.toasts.push(b),t.post(`/api/repo/${f.repoId}/refresh`).then(C=>{C.data.status=="ready"?b.title=`${f.repoId} is refreshed.`:b.title=`Refreshing of ${f.repoId}.`},C=>{b.title=`Error during the refresh of ${f.repoId}.`,b.body=C.body})},e.getGitHubRepositories=f=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(b=>{e.userInfo.repositories=b.data})};let w=null;e.watch("query",()=>{s.timeout.cancel(w),w=s.timeout(()=>{m(r.username)},500)},!0)},Xa=function(e,t,n){let r=ct(),s=Kt();e.Math=Math,e.watch("user.status",()=>{e.user==null&&n.url("/")}),e.user==null&&n.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let i=C=>{if(C.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){C.preventDefault();let k=document.querySelector('.admin-filter-toolbar input[type="search"]');k&&k.focus()}};s(document,"keydown",i),e.on("dispose",()=>document.removeEventListener("keydown",i)),e.clearFilter=C=>{C==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[C]="",e.query.page=1},e.chips=[];let a=()=>{let C=[];(e.query.dateFrom||e.query.dateTo)&&C.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=C};e.statusCountFor=C=>{let k=(e.statusCounts||[]).find(R=>R._id===C);return k?k.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=C=>{e.query.sort===C?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=C,e.query.direction="desc"),e.query.page=1},e.sortIcon=C=>e.query.sort===C?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",m={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!1,expired:!1,removed:!1,error:!0,preparing:!0},h=loadFilterPrefs(u)||{};e.query=Object.assign({},m,h,{page:1,search:""});let v=n.search();v.search&&(e.query.search=v.search);let w="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(w)||"[]"),e.savePreset=()=>{let C=window.prompt("Preset name:");if(!C)return;let k=Object.assign({},e.query);delete k.page,e.presets=(e.presets||[]).filter(R=>R.name!==C),e.presets.push({name:C,query:k}),localStorage.setItem(w,JSON.stringify(e.presets))},e.applyPreset=C=>{Object.assign(e.query,C.query,{page:1})},e.deletePreset=C=>{e.presets=(e.presets||[]).filter(k=>k.name!==C.name),localStorage.setItem(w,JS
Line truncated
|
||||
`).map((ve,me)=>me===0?ve:he+ve).join(`
|
||||
`)}else typeof K=="number"||typeof K=="boolean"?ue=String(K):ue=JSON.stringify(K);J.push(`${oe}${ue}${ne}`)}),J.push("}"),J.join(`
|
||||
`)}function h(D){let T=[],V="",_=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,O=0,I;for(;I=_.exec(D);)V+=D.slice(O,I.index),O=_.lastIndex,T.push({key:I[1],op:I[2]||"=",val:I[3]});return V+=D.slice(O),{filters:T,free:V.trim().toLowerCase()}}function v(D,T){for(let V of T.filters){let _=(I,J,Y)=>{let K=parseFloat(I),B=parseFloat(J);return Y==="="?String(I)===String(J):Y==="!="?String(I)!==String(J):Y===">="?K>=B:Y==="<="?K<=B:Y===">"?K>B:Y==="<"?K<B:!0},O;if(V.key==="code")O=D.displayMessage;else if(V.key==="module")O=D.module;else if(V.key==="status")O=D._status;else if(V.key==="url")O=D._url;else if(V.key==="repo")O=D._repoId;else if(V.key==="level")O=D.level;else continue;if(O==null||!_(O,V.val,V.op))return!1}return!(T.free&&((D.displayMessage||"")+" "+(D.module||"")+" "+(D._url||"")+" "+JSON.stringify(D.raw||[])).toLowerCase().indexOf(T.free)===-1)}function w(){let D=h(e.query.search||"");e.parsedFilterCount=D.filters.length;let T=e.query.bucket,V=e.entries.filter(O=>T&&O._bucket!==T?!1:v(O,D)),_=e.query.group;if(_){let O=Y=>_==="module"?Y.module:Y.displayMessage||Y.message||"_",I=new Map;for(let Y of V){let K=O(Y);if(I.has(K)){let B=I.get(K);B.count++,B._related.push(Y),new Date(Y.ts)>new Date(B.ts)&&(B.ts=Y.ts,B._url=Y._url,B._status=Y._status),new Date(Y.ts)<new Date(B._firstSeen)&&(B._firstSeen=Y.ts)}else{let B=Object.assign({},Y);B._key=`${_}:${K}`,B._related=[Y],B._firstSeen=Y.ts,B._lastHourCount=0,B.count=1,I.set(K,B)}}let J=Date.now()-3600*1e3;for(let Y of I.values())Y._lastHourCount=Y._related.filter(K=>new Date(K.ts).getTime()>=J).length;V=Array.from(I.values())}else V=V.map((O,I)=>(O._key="row:"+I+":"+O.ts,O._related=[O],O._firstSeen=O.ts,O._lastHourCount=0,O.count=1,O));e.query.sort==="count"?V.sort((O,I)=>I.count-O.count||new Date(I.ts)-new Date(O.ts)):V.sort((O,I)=>new Date(I.ts)-new Date(O.ts)),e.visible=V}function f(D){let T=D?e.entries.length:0,V=D?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:T,limit:V}}).then(_=>{let O=(_.data.entries||[]).map(u);e.entries=D?e.entries.concat(O):O,e.available=!!_.data.available,e.cap=_.data.max||e.cap,e.total=_.data.total||e.entries.length,w()},_=>console.error(_))}e.loadMore=()=>f(!0),e.canLoadMore=()=>e.entries.length<e.total;function b(){t.get("/api/admin/errors/stats").then(D=>{let T=D.data||{},V=T.prev24h?Math.round((T.last24h-T.prev24h)/T.prev24h*100):0;e.stats={last24h:T.last24h||0,prev24h:T.prev24h||0,delta:V,severity:T.severity||{error:0,warn:0,info:0},unique:T.unique||{error:0,warn:0,info:0},buckets:T.buckets||[],dropped:T.dropped||0}},D=>console.error(D))}function C(){f(),b()}e.barPx=(D,T)=>{let V=e.stats.buckets||[],_=0;for(let Y of V)_=Math.max(_,(Y.error||0)+(Y.warn||0)+(Y.info||0));if(!_)return 0;let O=(D.error||0)+(D.warn||0)+(D.info||0);if(!O)return 0;let I=Math.round(O/_*60),J=D[T]||0;return Math.round(J/O*I)},e.bucketTitle=D=>`${new Date(D.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${D.error||0} err \xB7 ${D.warn||0} warn \xB7 ${D.info||0} info`,e.toggle=D=>{e.expanded[D._key]=!e.expanded[D._key]},e.setBucket=D=>{e.query.bucket=D},e.refreshNow=C,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(C,D=>console.error(D))},e.exportCsv=()=>{let D=["ts","level","module","displayMessage","_status","_url","_repoId"],T=[D.join(",")];for(let I of e.visible)T.push(D.map(J=>{let Y=I[J]==null?"":String(I[J]);return/[",\n]/.test(Y)?`"${Y.replace(/"/g,'""')}"`:Y}).join(","));let V=new Blob([T.join(`
|
||||
`)],{type:"text/csv;charset=utf-8"}),_=URL.createObjectURL(V),O=document.createElement("a");O.href=_,O.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(O),O.click(),document.body.removeChild(O),URL.revokeObjectURL(_)};function k(D){e.copyHint=`${D} copied`,s.timeout(()=>{e.copyHint=""},1500)}e.copyJson=D=>{navigator.clipboard.writeText(D._detailJson).then(()=>k("JSON"))},e.copyCurl=D=>{if(!D._url)return;let V=`curl -X ${D._method||"GET"} '${window.location.origin}${D._url}'`;navigator.clipboard.writeText(V).then(()=>k("curl"))},C();let R=r(()=>{e.query.autoRefresh&&C()},15e3);e.on("dispose",()=>r.cancel(R)),e.watch("query.search",w),e.watch("query.bucket",w),e.watch("query.sort",w),e.watch("query.group",w)},el=function(e,t,n,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&n.url("/")}),e.user==null){n.url("/");return}e.data=null,e.loading=!0,e.error=null;function s(f){if(f==null)return"\u2014";for(var b=["B","KB","MB","GB","TB"],C=0,k=f;k>=1024&&C<b.length-1;)k/=1024,C++;return k.toFixed(C>0?1:0)+" "+b[C]}e.humanBytes=s;function i(f){if(!f)return"\u2014";var b=Math.floor(f/86400),C=Math.floor(f%86400/3600),k=Math.floor(f%3600/60);return b>0?b+"d "+(C<10?"0":"")+C+"h":C>0?C+"h "+(k<10?"0":"")+k+"m":k+"m"}e.humanDuration=i;function a(f){return f==null?"\u2014":f>=1e6?(f/1e6).toFixed(1)+"M":f>=1e3?(f/1e3).toFixed(1)+"K":String(f)}e.humanNum=a,e.queueTotal=function(f){return f?(f.waiting||0)+(f.active||0)+(f.delayed||0)+(f.failed||0):0},e.statusCount=function(f){if(!e.data||!e.data.repos)return 0;for(var b=e.data.repos.statusBreakdown||[],C=0;C<b.length;C++)if(b[C]._id===f)return b[C].count;return 0},e.barPct=function(f){var b=e.data&&e.data.repos?e.data.repos.total:0;if(!b)return 0;var C=[f];f==="expired"&&C.push("expiring"),f==="removed"&&C.push("removing"),f==="preparing"&&C.push("download");var k=0;return C.forEach(function(R){k+=e.statusCount(R)}),Math.max(.4,k/b*100)},e.errPct=function(f){if(!e.data||!e.data.errors)return 0;var b=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[f]/b*100};function u(f){var b=f||[];return b.map(function(C,k){var R=b[k-1]||{},D=Object.assign({},C);return D.dailyRepositories=k?Math.max(0,(C.nbRepositories||0)-(R.nbRepositories||0)):0,D.dailyUsers=k?Math.max(0,(C.nbUsers||0)-(R.nbUsers||0)):0,D.dailyPageViews=k?Math.max(0,(C.nbPageViews||0)-(R.nbPageViews||0)):0,D})}function m(f){var b=f&&f.length?f[f.length-1]:{};return{repositories:b.dailyRepositories||0,users:b.dailyUsers||0,pageViews:b.dailyPageViews||0}}var h={};e.historyBarH=function(f,b){return!f||!h[b]?0:Math.max(1,Math.round(f[b]/h[b]*140))},e.historyLabel=function(f){if(!f||!f.date)return"";var b=new Date(f.date);return b.getUTCMonth()+1+"/"+b.getUTCDate()};function v(){t.get("/api/admin/overview").then(function(f){f.data.history=u(f.data.history),f.data.daily={today:m(f.data.history)},e.data=f.data,e.loading=!1,e.error=null,h={},(f.data.history||[]).forEach(function(b){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(C){(!h[C]||b[C]>h[C])&&(h[C]=b[C])})})},function(f){e.loading=!1,e.error=f.data&&f.data.error||"Failed to load overview"})}v();var w=r(v,3e4);e.on("dispose",function(){r.cancel(w)})};var tl=[{path:"/connections",template:"partials/connections.htm",title:"GitHub connections \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Wa(e,t.http)},{path:"/signin",template:"partials/signin.htm",title:"Sign in \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ka(e,t.http)},{path:"/",template:"partials/home.htm",title:"Anonymous GitHub \u2013 Share the code, not the author",preserveExplorer:!1,setup:(e,t)=>Ua(e,t.http,t.location,t.window,t.timeout)},{path:"/dashboard",template:"partials/dashboard.htm",title:"Your anonymizations \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Fa(e,t.http,t.location,t.promises,t.window,t.quotaService)},{path:"/pr-dashboard",redirect:"/dashboard"},{path:"/anonymize/:repoId?",template:"partials/anonymize.htm",title:"New anonymization \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>en(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/pull-request-anonymize/:pullRequestId?",template:"partials/anonymize.htm",title:"Anonymize a pull request \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>en(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/gist-anonymize/:gistId?",template:"partials/anonymize.htm",title:"Anonymize a gist \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>en(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/status/:repoId",template:"partials/status.htm",title:"Repository status \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>za(e,t.http,t.params)},{path:"/conferences",template:"partials/conferences.htm",title:"Your conferences \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>ja(e,t.http,t.location)
Line truncated
|
||||
anonymous_github`)])])])])]),p(" Limitations "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-warning"})]),g("Limitations ")]),o("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[o("h3",{class:"panel-title"},[o("a",{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? ")])]),o("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[o("div",{class:"panel-body p-3"},[o("ul",null,[o("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),o("li",null," Files larger than 8 MB are not supported. "),o("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),o("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),p(" Privacy & Security "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-shield"})]),g("Privacy & Security ")]),o("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),o("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[o("div",{class:"panel-body p-3"},[o("p",null," Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the anonymization service. When a repository is removed or expires, only its configuration is retained \u2014 this makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[o("h3",{class:"panel-title"},[o("a",{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? ")])]),o("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[o("div",{class:"panel-body p-3"},[o("p",null,[g("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),o("a",{href:"/connections"},"GitHub connections"),g(" page.")]),o("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),o("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[o("h3",{class:"panel-title"},[o("a",{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? ")])]),o("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[o("div",{class:"panel-body p-3"},[o("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),p(" Self-Hosting "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#project-icon-storage"})]),g("Self-Hosting ")]),o("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[o("h3",{class:"panel-title"},[o("a",{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? ")])]),o("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[o("div",{class:"panel-body p-3"},[o("p",null,[g(" Yes. Anonymous Git
Line truncated
|
||||
anonymized repository often think it is about the paper, so the
|
||||
label names the service and the tooltip says who to contact. `),t[12]||(t[12]=s("li",{class:"nav-item d-none d-lg-block"},[s("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"},[s("i",{class:"fas fa-bug d-lg-none mr-1","aria-hidden":"true"}),y("Report a bug ")])],-1)),e.isDarkMode?f("v-if",!0):(l(),d("li",tA,[s("a",{class:"nav-link nav-icon",href:"#",title:"Switch to dark mode","aria-label":"Switch to dark mode",role:"button",onClick:t[0]||(t[0]=ke(o=>{e.darkMode(!0)},["prevent"]))},[...t[7]||(t[7]=[s("i",{class:"fas fa-moon","aria-hidden":"true"},null,-1)])])])),e.isDarkMode?(l(),d("li",sA,[s("a",{class:"nav-link nav-icon",href:"#",title:"Switch to light mode","aria-label":"Switch to light mode",role:"button",onClick:t[1]||(t[1]=ke(o=>{e.darkMode(!1)},["prevent"]))},[...t[8]||(t[8]=[s("i",{class:"fas fa-sun","aria-hidden":"true"},null,-1)])])])):f("v-if",!0),e.user?f("v-if",!0):(l(),d("li",nA,[...t[9]||(t[9]=[s("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[s("i",{class:"fab fa-github mr-1"}),y(" Sign in ")],-1)])])),e.user?(l(),d("li",oA,[s("a",rA,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,iA)):f("v-if",!0),s("span",aA,c(e.user?.username),1)]),s("div",lA,[s("h6",dA,"Signed in as @"+c(e.user?.username),1),t[10]||(t[10]=be('<a class="dropdown-item" href="/profile"><i class="fas fa-sliders-h" aria-hidden="true"></i><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><i class="fas fa-link" aria-hidden="true"></i><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><i class="fas fa-bug" aria-hidden="true"></i><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="/api/user/logout" target="_self"><i class="fas fa-sign-out-alt" aria-hidden="true"></i><span>Sign out</span></a>',8))])])):f("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,uA)):f("v-if",!0)],64)}var cA={class:"paper-landing"},pA={class:"paper-hero"},fA={class:"paper-hero-copy"},mA={class:"paper-cta-row"},hA={key:0,href:"/signin",target:"_self",class:"btn-hero"},vA={key:1,href:"/anonymize",class:"btn-hero"},yA={class:"paper-stats paper-wrap",id:"metrics","aria-label":"Usage over the last 60 days"},gA={class:"paper-stats-grid"},bA={class:"paper-stat-value"},wA={class:"paper-stat-label"},kA=["viewBox"],_A=["x","y","width","height"],EA={key:1,class:"paper-stat-delta"},CA={class:"paper-features paper-wrap","aria-label":"Product features"},NA={class:"paper-features-list",role:"tablist","aria-label":"Features","aria-orientation":"vertical"},SA=["onClick","onKeydown","id","aria-controls","aria-selected","tabindex"],DA={class:"paper-eyebrow"},RA={class:"paper-feature-title"},TA={class:"paper-feature-more"},OA={class:"paper-feature-title paper-feature-title-m","aria-hidden":"true"},AA=["href","target"],IA={class:"paper-features-stage"},VA=["id","aria-labelledby"],PA={class:"paper-frame"},qA={class:"paper-frame-bar","aria-hidden":"true"},MA={class:"url"},$A=["src","alt"],UA={class:"paper-footer"},LA={class:"paper-footer-inner"},FA={class:"paper-footer-col"},zA={key:0,href:"/dashboard"},HA={key:1,href:"/conferences"},BA={class:"paper-footer-foot"};function fl(e,t){return l(),d("div",cA,[f(" Hero: copy on the left, the product on the right "),s("section",pA,[s("div",fA,[t[2]||(t[2]=s("div",{class:"paper-eyebrow"},"For double-blind peer review",-1)),t[3]||(t[3]=s("h1",{class:"hero-title"},[y(" Share the code,"),s("br"),s("span",{class:"accent"},"not the author.")],-1)),t[4]||(t[4]=s("p",{class:"hero-subtitle"}," Anonymous GitHub makes a read-only mirror of your repository, pull request, or gist with every trace of identity removed, so you can attach a link to your submission without breaking anonymity. ",-1)),s("div",mA,[e.user?f("v-if",!0):(l(),d("a",hA,[...t[0]||(t[0]=[s("i",{class:"fab fa-github mr-2","aria-hidden":"true"},null,-1),y("Sign in with GitHub ",-1)])])),e.user?(l(),d("a",vA," Anonymize a repository \
Line truncated
|
||||
label names the service and the tooltip says who to contact. `),t[12]||(t[12]=o("li",{class:"nav-item d-none d-lg-block"},[o("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"},[o("svg",{class:"ui-icon d-lg-none mr-1","aria-hidden":"true"},[o("use",{href:"#ui-icon-bug"})]),g("Report a bug ")])],-1)),e.isDarkMode?p("v-if",!0):(l(),d("li",QI,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to dark mode","aria-label":"Switch to dark mode",role:"button",onClick:t[0]||(t[0]=ce(n=>{e.darkMode(!0)},["prevent"]))},[...t[7]||(t[7]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",XI,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to light mode","aria-label":"Switch to light mode",role:"button",onClick:t[1]||(t[1]=ce(n=>{e.darkMode(!1)},["prevent"]))},[...t[8]||(t[8]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-sun"})],-1)])])])):p("v-if",!0),e.user?p("v-if",!0):(l(),d("li",ZI,[...t[9]||(t[9]=[o("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[o("i",{class:"fab fa-github mr-1"}),g(" Sign in ")],-1)])])),e.user?(l(),d("li",xI,[o("a",e4,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,t4)):p("v-if",!0),o("span",o4,c(e.user?.username),1)]),o("div",s4,[o("h6",n4,"Signed in as @"+c(e.user?.username),1),t[10]||(t[10]=le('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a><a class="dropdown-item" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg><span>Support the project</span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="/api/user/logout" target="_self"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-logout"></use></svg><span>Sign out</span></a>',9))])])):p("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,r4)):p("v-if",!0)],64)}var i4={class:"paper-landing"},a4={class:"paper-hero"},l4={class:"paper-hero-copy"},d4={class:"paper-cta-row"},u4={key:0,href:"/signin",target:"_self",class:"btn-hero"},c4={key:1,href:"/anonymize",class:"btn-hero"},p4={class:"paper-stats paper-wrap",id:"metrics","aria-label":"Usage over the last 60 days"},f4={class:"paper-stats-grid"},m4={class:"paper-stat-value"},h4={class:"paper-stat-label"},v4=["viewBox"],g4=["x","y","width","height"],y4={key:1,class:"paper-stat-delta"},b4={class:"paper-features paper-wrap","aria-label":"Product features"},w4={class:"paper-features-list",role:"tablist","aria-label":"Features","aria-orientation":"vertical"},k4=["onClick","onKeydown","id","aria-controls","aria-selected","tabindex"],E4={class:"paper-eyebrow"},C4={class:"paper-feature-title"},_4={class:"paper-feature-more"},S4={class:"paper-feature-title paper-feature-title-m","aria-hidden":"true"},N4=["href","target"],D4={class:"paper-features-stage"},R4=["id","aria-labelledby"],T4={class:"paper-frame"},A4={class:"paper-frame-bar","aria-hidden":"true"},O4={class:"url"},I4=["src","alt"],P4={class:"paper-footer"},V4={class:"paper-footer-inner"},q4={class:"paper-footer-col"},M4={key:0,href:"/dashboard"},$4={key:1,href:"/conferences"},L4={class:"paper-footer-foot"};function El(e,t){return l(),d("div",i4,[p(" Hero: copy on the left, the product on the right "),o("section",a4,[o("div",l4,[t[2]||(t[2]=o("div",{class:"paper-eyebrow"},"For double-blind peer review",-1)),t[3]||(t[3]=o("h1",{class:"hero-title"},[g(" Share the code,"),o("br"),o("span",{class:"accent"},"not the author.")],-1)),t[4]||(t[4]=o("p",{class:"hero-subtitle
Line truncated
|
||||
MIT CSAIL
|
||||
jane.smith@example.org`},BP={id:"termsHelp",class:"form-text text-muted"},GP={class:"invalid-feedback"},jP={id:"settings-display",class:"paper-settings-section"},WP={class:"form-group paper-check-list"},KP={class:"form-check"},YP={class:"form-check-input",type:"checkbox",id:"link",name:"link"},JP={class:"form-check"},QP={class:"form-check-input",type:"checkbox",id:"image",name:"image"},XP={class:"form-check"},ZP={class:"form-check-input",type:"checkbox",id:"pdf",name:"pdf"},xP={class:"form-check"},e3={class:"form-check-input",type:"checkbox",id:"notebook",name:"notebook"},t3={id:"settings-features",class:"paper-settings-section"},s3={class:"form-group paper-check-list"},n3={class:"form-check"},o3={class:"form-check-input",type:"checkbox",id:"page",name:"page"},r3={class:"form-check"},i3={class:"form-check-input",type:"checkbox",id:"loc",name:"loc"},a3={class:"form-check"},l3={class:"form-check-input",type:"checkbox",id:"update",name:"update"},d3={class:"form-group"},u3={class:"form-control",id:"mode",name:"mode"},c3={class:"form-text text-muted"},p3={id:"settings-expiration",class:"paper-settings-section"},f3={class:"form-group"},m3={class:"form-control",id:"expiration",name:"expiration"},h3={class:"paper-settings-footer"},v3={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},y3=["textContent"],g3={class:"paper-settings-actions"},b3=["disabled"],w3={key:0,class:"fas fa-check mr-1","aria-hidden":"true"},k3={id:"settings-account",class:"paper-settings-section"},_3={class:"paper-account-card"},E3=["src"],C3={class:"paper-account-identity"},N3={class:"paper-account-name"},S3={class:"paper-danger-zone"},D3={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},R3=["textContent"],T3=["disabled"];function gl(e,t){let o=we("paper-scrollspy"),r=we("field"),n=we("form");return l(),d("div",_P,[t[48]||(t[48]=be('<div class="paper-crumbs"><a href="/dashboard">My work</a> \xA0/\xA0 <span class="here">Settings</span></div><h1 class="paper-page-title">Your <em>settings</em></h1><p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>',3)),s("div",EP,[E((l(),d("aside",CP,[...t[2]||(t[2]=[be('<div class="paper-settings-toc-head">On this page</div><nav><a href="#settings-quota">Quota</a><a href="#settings-terms">Terms to redact</a><a href="#settings-display">Display</a><a href="#settings-features">Features</a><a href="#settings-expiration">Expiration</a><a href="#settings-account">Account</a></nav>',2)])])),[[o]]),s("div",NP,[f(" Quota: same markup as the dashboard, driven by quotaService "),s("section",SP,[t[4]||(t[4]=s("div",{class:"paper-section-eyebrow"},"Quota",-1)),t[5]||(t[5]=s("p",{class:"paper-section-copy"},"What you are using across all your anonymizations. Conferences can lift these limits during a review window.",-1)),e.quota?(l(),d("div",DP,[(l(),d(J,null,ne([{key:"repository",label:"Repositories",kind:"count"},{key:"storage",label:"Storage",kind:"bytes"},{key:"file",label:"Files",kind:"count"}],(i,a)=>s("div",RP,[s("div",TP,[s("span",OP,c(i?.label),1),i?.kind==="count"?(l(),d("span",AP,[y(c(e.fmt?.number(e.quota[i?.key].used)),1),e.quota[i?.key].unlimited?f("v-if",!0):(l(),d("span",IP," / "+c(e.fmt?.number(e.quota[i?.key].total)),1)),e.quota[i?.key].unlimited?(l(),d("span",VP,"Unlimited")):f("v-if",!0)])):f("v-if",!0),i?.kind==="bytes"?(l(),d("span",PP,[y(c(e.fmt?.humanFileSize(e.quota[i?.key].used)),1),e.quota[i?.key].unlimited?f("v-if",!0):(l(),d("span",qP," / "+c(e.fmt?.humanFileSize(e.quota[i?.key].total)),1)),e.quota[i?.key].unlimited?(l(),d("span",MP,"Unlimited")):f("v-if",!0)])):f("v-if",!0)]),s("div",{class:O(["quota-track","quota-"+e.quota[i?.key].level]),role:"progressbar","aria-valuemin":"0","aria-label":i?.label+" quota","aria-valuenow":e.quota[i?.key].used,"aria-valuemax":e.quota[i?.key].unlimited?e.quota[i?.key].used:e.quota[i?.key].total,"aria-valuetext":e.quota[i?.key].unlimited?"unlimited":e.fmt.number(e.quota[i?.key].percent,0)+"% used"},[e.quota[i?.key].unlimited?f("v-if",!0):(l(),d("div",{key:0,class:"quota-fill",style:qe({width:e.quota[i?.key].percent+"%"})},null,4))],10,$P)])),64))])):f("v-if",!0),e.quota?f("v-if",!0):(l(),d("div",UP,[(l(),d(J,null,ne([1,2,3],(i,a)=>s("div",LP,[...t[3]||(t[3]=[s("div",{class:"quota-header"},[s("span",{class:"skeleton skeleton-line",style:{width:"40%"}}),s("span",{class:"skeleton skeleton-line",style:{width:"25%"}})],-1),s("div",{class:"quota-track"},null,-1)])])),64))]))]),E((l(),d("form",{class:"form needs-validation",name:"defaultsForm",novalidate:"",onSubmit:t[0]||(t[0]=ke(i=>e.submitForm(i,()=>{e.saveDefault(i)}),["prevent"]))},[s("section",FP,[t[11]||(t[11]=s("div",{class:"paper-section-eyebrow"},"Terms to redact",-1)),t[12]||(t[12]=s("p",{class:"paper-section-copy"},"Pre-filled in every new anonymization. You can still edit the list per anonymization.",-1)),s("div",zP,[t[10]||(t[10]=s("label",{class:"paper-field-label",for:"terms"},"Default terms",-1)),E(s("tex
Line truncated
|
||||
`);for(let k=0;k<v.length;k++){let p=v[k];if(p.startsWith("diff --git")){h(),m();continue}if(p.startsWith("--- ")){i&&i.lines.length&&h(),m().oldPath=p.replace(/^--- (a\/)?/,"").trim();continue}if(p.startsWith("+++ ")){m().newPath=p.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(p.startsWith("index ")||p.startsWith("similarity index")||p.startsWith("rename ")||p.startsWith("new file mode")||p.startsWith("deleted file mode")||p.startsWith("Binary files"))){if(p.startsWith("@@")){let g=p.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);g&&(a=parseInt(g[1],10),u=parseInt(g[2],10)),m().lines.push({kind:"hunk",oldNo:"",newNo:"",text:p});continue}i&&(p.startsWith("+")?(i.lines.push({kind:"add",oldNo:"",newNo:u,text:p.slice(1)}),u++):p.startsWith("-")?(i.lines.push({kind:"remove",oldNo:a,newNo:"",text:p.slice(1)}),a++):(i.lines.push({kind:"ctx",oldNo:a,newNo:u,text:p.startsWith(" ")?p.slice(1):p}),a++,u++))}}return o(n,i),n.join("")}})({trustAsHtml:e=>e});function Fq(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function zq(e){return String(e??"").toUpperCase()}function Hq(e,t,o=0){return(e||[]).slice(o,o+t)}function Bq(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function Gq(e,t){if(!t)return e||[];let o=Array.isArray(t)?t:[t];return[...e||[]].sort((r,n)=>{for(let i of o){let a=i[0]==="-"?-1:1;i=i.replace(/^[+-]/,"");let u=i.split(".").reduce((h,v)=>h?.[v],r),m=i.split(".").reduce((h,v)=>h?.[v],n);if(typeof u=="string"&&(u=u.toLowerCase()),typeof m=="string"&&(m=m.toLowerCase()),u!==m)return(u==null?-1:m==null?1:u<m?-1:1)*a}return 0})}function jq(e,t){if(!e)return"";let o=new Date(e);if(isNaN(o))return String(e);if(!t||t==="mediumDate")return o.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let r={yyyy:o.getFullYear(),MMM:o.toLocaleDateString(void 0,{month:"short"}),MM:String(o.getMonth()+1).padStart(2,"0"),dd:String(o.getDate()).padStart(2,"0"),d:o.getDate(),HH:String(o.getHours()).padStart(2,"0"),mm:String(o.getMinutes()).padStart(2,"0"),ss:String(o.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,n=>r[n])}function Wq(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var Nl={ERRORS:{repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"Unknown error, contact the admin.",unreachable:"Anonymous GitHub is unreachable, contact the admin.",request_error:"Unable to download the file, check your connection or contact the admin.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"The repository was not found on GitHub. Check the URL and spelling, make sure you are signed in to the account that can see it, and confirm the repo isn't hidden under an org that restricts third-party app access.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name is invalid; expected the form 'owner/name'.",repoId_not_defined:"A repository ID must be provided.",repoUrl_not_defined:"The repository URL needs to be defined.",source_not_provided:"A repository source must be provided.",github_rate_limit_exceeded:"GitHub
Line truncated
|
||||
jane.smith@example.org`},RV={id:"termsHelp",class:"form-text text-muted"},TV={class:"invalid-feedback"},AV={id:"settings-display",class:"paper-settings-section"},OV={class:"form-group paper-check-list"},IV={class:"form-check"},PV={class:"form-check-input",type:"checkbox",id:"link",name:"link"},VV={class:"form-check"},qV={class:"form-check-input",type:"checkbox",id:"image",name:"image"},MV={class:"form-check"},$V={class:"form-check-input",type:"checkbox",id:"pdf",name:"pdf"},LV={class:"form-check"},UV={class:"form-check-input",type:"checkbox",id:"notebook",name:"notebook"},FV={id:"settings-features",class:"paper-settings-section"},zV={class:"form-group paper-check-list"},HV={class:"form-check"},BV={class:"form-check-input",type:"checkbox",id:"page",name:"page"},jV={class:"form-check"},GV={class:"form-check-input",type:"checkbox",id:"loc",name:"loc"},WV={class:"form-check"},KV={class:"form-check-input",type:"checkbox",id:"update",name:"update"},YV={class:"form-group"},JV={class:"form-control",id:"mode",name:"mode"},QV={class:"form-text text-muted"},XV={id:"settings-expiration",class:"paper-settings-section"},ZV={class:"form-group"},xV={class:"form-control",id:"expiration",name:"expiration"},e5={key:0,class:"draft-status",role:"status"},t5={key:1,class:"draft-status"},o5={class:"paper-settings-footer"},s5={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},n5=["textContent"],r5={class:"paper-settings-actions"},i5=["disabled"],a5={key:0,class:"ui-icon mr-1","aria-hidden":"true"},l5={id:"settings-account",class:"paper-settings-section"},d5={class:"paper-account-card"},u5=["src"],c5={class:"paper-account-identity"},p5={class:"paper-account-name"},f5={class:"paper-danger-zone"},m5={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},h5=["textContent"],v5=["disabled"];function Dl(e,t){let n=We("quota-usage"),r=ye("paper-scrollspy"),s=ye("field"),i=ye("form");return l(),d("div",yV,[t[52]||(t[52]=le('<div class="paper-crumbs"><a href="/dashboard">My work</a> \xA0/\xA0 <span class="here">Settings</span></div><h1 class="paper-page-title">Your <em>settings</em></h1><p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>',3)),o("div",bV,[N((l(),d("aside",wV,[...t[4]||(t[4]=[le('<div class="paper-settings-toc-head">On this page</div><nav><a href="#settings-quota">Quota</a><a href="#settings-terms">Terms to redact</a><a href="#settings-display">Display</a><a href="#settings-features">Features</a><a href="#settings-expiration">Expiration</a><a href="#settings-account">Account</a></nav>',2)])])),[[r]]),o("div",kV,[p(" Account usage shares the dashboard component. "),o("section",EV,[t[6]||(t[6]=o("div",{class:"paper-section-eyebrow"},"Quota",-1)),t[7]||(t[7]=o("p",{class:"paper-section-copy"},"What you are using across all your anonymizations. Conferences can lift these limits during a review window.",-1)),e.quota?(l(),qt(n,{key:0,quota:e.quota,fmt:e.fmt},null,8,["quota","fmt"])):p("v-if",!0),e.quota?p("v-if",!0):(l(),d("div",CV,[(l(),d(Q,null,se([1,2,3],(a,u)=>o("div",_V,[...t[5]||(t[5]=[o("div",{class:"quota-header"},[o("span",{class:"skeleton skeleton-line",style:{width:"40%"}}),o("span",{class:"skeleton skeleton-line",style:{width:"25%"}})],-1),o("div",{class:"quota-track"},null,-1)])])),64))]))]),N((l(),d("form",{class:"form needs-validation",name:"defaultsForm",novalidate:"",onSubmit:t[2]||(t[2]=ce(a=>e.submitForm(a,()=>{e.saveDefault(a)}),["prevent"]))},[o("section",SV,[t[13]||(t[13]=o("div",{class:"paper-section-eyebrow"},"Terms to redact",-1)),t[14]||(t[14]=o("p",{class:"paper-section-copy"},"Pre-filled in every new anonymization. You can still edit the list per anonymization.",-1)),o("div",NV,[t[12]||(t[12]=o("label",{class:"paper-field-label",for:"terms"},"Default terms",-1)),N(o("textarea",DV,null,512),[[s,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),o("small",RV,[t[8]||(t[8]=g(" One term per line (regex allowed). Each match is replaced by ",-1)),o("code",null,c(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[9]||(t[9]=g(", or use ",-1)),t[10]||(t[10]=o("code",null,"term=>replacement",-1)),t[11]||(t[11]=g(" to pick your own. ",-1))]),N(o("div",TV," Terms are in an invalid format ",512),[[W,e.defaultsForm?.terms?.errors?.format]])])]),o("section",AV,[t[23]||(t[23]=o("div",{class:"paper-section-eyebrow"},"Display",-1)),t[24]||(t[24]=o("p",{class:"paper-section-copy"},"What reviewers can see inside an anonymized repository.",-1)),o("div",OV,[o("div",IV,[N(o("input",PV,null,512),[[s,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[15]||(t[15]=o("label",{class:"form-check-label",for:"link"},"Keep links",-1)),t[16]||(t[16]=o("small",{class:"form-text text-muted"},"Unchecked, every link in text files is removed.",-1))]),o("div",VV,[N(o("input",qV,null,512),[[s,{state:e.viewState,set:a=>{e.options.image=a},value:e.op
Line truncated
|
||||
`);for(let w=0;w<v.length;w++){let f=v[w];if(f.startsWith("diff --git")){h(),m();continue}if(f.startsWith("--- ")){i&&i.lines.length&&h(),m().oldPath=f.replace(/^--- (a\/)?/,"").trim();continue}if(f.startsWith("+++ ")){m().newPath=f.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(f.startsWith("index ")||f.startsWith("similarity index")||f.startsWith("rename ")||f.startsWith("new file mode")||f.startsWith("deleted file mode")||f.startsWith("Binary files"))){if(f.startsWith("@@")){let b=f.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);b&&(a=parseInt(b[1],10),u=parseInt(b[2],10)),m().lines.push({kind:"hunk",oldNo:"",newNo:"",text:f});continue}i&&(f.startsWith("+")?(i.lines.push({kind:"add",oldNo:"",newNo:u,text:f.slice(1)}),u++):f.startsWith("-")?(i.lines.push({kind:"remove",oldNo:a,newNo:"",text:f.slice(1)}),a++):(i.lines.push({kind:"ctx",oldNo:a,newNo:u,text:f.startsWith(" ")?f.slice(1):f}),a++,u++))}}return n(s,i),s.join("")}})({trustAsHtml:e=>e});function Oq(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function Iq(e){return String(e??"").toUpperCase()}function Pq(e,t,n=0){return(e||[]).slice(n,n+t)}function Vq(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function qq(e,t){if(!t)return e||[];let n=Array.isArray(t)?t:[t];return[...e||[]].sort((r,s)=>{for(let i of n){let a=i[0]==="-"?-1:1;i=i.replace(/^[+-]/,"");let u=i.split(".").reduce((h,v)=>h?.[v],r),m=i.split(".").reduce((h,v)=>h?.[v],s);if(typeof u=="string"&&(u=u.toLowerCase()),typeof m=="string"&&(m=m.toLowerCase()),u!==m)return(u==null?-1:m==null?1:u<m?-1:1)*a}return 0})}function Mq(e,t){if(!e)return"";let n=new Date(e);if(isNaN(n))return String(e);if(!t||t==="mediumDate")return n.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let r={yyyy:n.getFullYear(),MMM:n.toLocaleDateString(void 0,{month:"short"}),MM:String(n.getMonth()+1).padStart(2,"0"),dd:String(n.getDate()).padStart(2,"0"),d:n.getDate(),HH:String(n.getHours()).padStart(2,"0"),mm:String(n.getMinutes()).padStart(2,"0"),ss:String(n.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,s=>r[s])}function $q(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var ql={ERRORS:{repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"Unknown error, contact the admin.",unreachable:"Anonymous GitHub is unreachable, contact the admin.",request_error:"Unable to download the file, check your connection or contact the admin.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"The repository was not found on GitHub. Check the URL and spelling, make sure you are signed in to the account that can see it, and confirm the repo isn't hidden under an org that restricts third-party app access.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name is invalid; expected the form 'owner/name'.",repoId_not_defined:"A repository ID must be provided.",repoUrl_not_defined:"The repository URL needs to be defined.",source_not_provided:"A repository source must be provided.",github_rate_limit_exceeded:"GitHub
Line truncated
|
||||
/*! Bundled license information:
|
||||
|
||||
@vue/shared/dist/shared.esm-bundler.js:
|
||||
|
||||
@@ -30,6 +30,7 @@ const UserSchema = new Schema({
|
||||
},
|
||||
],
|
||||
photo: String,
|
||||
projectNames: { type: Map, of: String },
|
||||
repositories: [
|
||||
{
|
||||
type: String,
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface IUser {
|
||||
default: boolean;
|
||||
}[];
|
||||
photo?: string;
|
||||
projectNames?: Map<string, string>;
|
||||
|
||||
repositories?: number[];
|
||||
default?: {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createHash } from "crypto";
|
||||
import AnonymousError from "../../core/AnonymousError";
|
||||
import User from "../../core/User";
|
||||
import UserModel from "../../core/model/users/users.model";
|
||||
|
||||
export function projectNameKey(type: string, id: string): string {
|
||||
return createHash("sha256").update(`${type}:${id}`).digest("hex");
|
||||
}
|
||||
|
||||
// Names belong to the user's dashboard, rather than the shared artifact.
|
||||
export async function saveProjectName(user: User, input: unknown): Promise<string> {
|
||||
const body = input as { type?: unknown; id?: unknown; name?: unknown } | null;
|
||||
if (!body || typeof body.type !== "string" || !["repo", "pr", "gist"].includes(body.type)
|
||||
|| typeof body.id !== "string" || !body.id || body.id.length > 250
|
||||
|| typeof body.name !== "string" || body.name.trim().length > 100
|
||||
|| Array.from(body.name).some(character => {
|
||||
const code = character.charCodeAt(0);
|
||||
return code < 32 || code === 127;
|
||||
})) {
|
||||
throw new AnonymousError("invalid_project_name", { httpStatus: 400 });
|
||||
}
|
||||
const type = String(body.type);
|
||||
const records = type === "repo" ? await user.getRepositories()
|
||||
: type === "pr" ? await user.getPullRequests() : await user.getGists();
|
||||
const field = type === "repo" ? "repoId" : type === "pr" ? "pullRequestId" : "gistId";
|
||||
if (!records.some(record => (record as unknown as Record<string, unknown>)[field] === body.id)) {
|
||||
throw new AnonymousError("not_authorized", { httpStatus: 403 });
|
||||
}
|
||||
const key = projectNameKey(type, body.id);
|
||||
const name = body.name.trim();
|
||||
const path = `projectNames.${key}`;
|
||||
await UserModel.updateOne({ _id: user.model._id }, name
|
||||
? { $set: { [path]: name } } : { $unset: { [path]: "" } });
|
||||
if (name) user.model.set(path, name);
|
||||
else user.model.projectNames?.delete(key);
|
||||
return name;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import FileModel from "../../core/model/files/files.model";
|
||||
import { isConnected } from "../database";
|
||||
import { octokit } from "../../core/GitHubUtils";
|
||||
import { createLogger, serializeError } from "../../core/logger";
|
||||
import { projectNameKey, saveProjectName } from "./project-names";
|
||||
|
||||
const logger = createLogger("user");
|
||||
|
||||
@@ -124,6 +125,15 @@ router.get("/quota", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/project-name", async (req, res) => {
|
||||
try {
|
||||
const user = await getUser(req);
|
||||
res.json({ name: await saveProjectName(user, req.body) });
|
||||
} catch (error) {
|
||||
handleError(error, res, req);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/default", async (req, res) => {
|
||||
try {
|
||||
const user = await getUser(req);
|
||||
@@ -214,6 +224,7 @@ router.delete("/", async (req, res) => {
|
||||
externalIDs: "",
|
||||
photo: "",
|
||||
default: "",
|
||||
projectNames: "",
|
||||
},
|
||||
}
|
||||
).exec();
|
||||
@@ -239,6 +250,7 @@ router.get(
|
||||
(await user.getRepositories()).map((x) => {
|
||||
const json = x.toJSON() as Record<string, unknown>;
|
||||
json.role = x.owner.id === user.model.id ? "owner" : "coauthor";
|
||||
json.projectName = user.model.projectNames?.get(projectNameKey("repo", x.repoId));
|
||||
return json;
|
||||
})
|
||||
);
|
||||
@@ -254,7 +266,7 @@ router.get(
|
||||
const user = await getUser(req);
|
||||
res.json(
|
||||
(await user.getGists()).map((x) => {
|
||||
return x.toJSON();
|
||||
return { ...x.toJSON(), projectName: user.model.projectNames?.get(projectNameKey("gist", x.gistId)) };
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -269,7 +281,7 @@ router.get(
|
||||
const user = await getUser(req);
|
||||
res.json(
|
||||
(await user.getPullRequests()).map((x) => {
|
||||
return x.toJSON();
|
||||
return { ...x.toJSON(), projectName: user.model.projectNames?.get(projectNameKey("pr", x.pullRequestId)) };
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,6 +14,7 @@ describe("dashboard refresh feedback", () => {
|
||||
let reloads = 0;
|
||||
vm.runInNewContext(actions, {
|
||||
state, reactive: value => value,
|
||||
statusKey: status => ["ready", "error", "removed", "expired"].includes(status) ? status : "progress",
|
||||
http: {
|
||||
post: async () => { if (postError) throw postError; return {}; },
|
||||
get: async () => { if (getError) throw getError; return { data: statuses.shift() }; },
|
||||
|
||||
@@ -93,10 +93,11 @@ describe("dashboard UI", function () {
|
||||
expect(divider).to.be.greaterThan(-1);
|
||||
});
|
||||
it("labels hidden statuses as hidden, not as active filters", function () {
|
||||
expect(html).to.match(/Hiding \{\{\s*statusKeyLabels\[f\]\s*\}\}/);
|
||||
expect(html).to.match(/\{\{\s*statusKeyLabels\[f\]\s*\}\} hidden/);
|
||||
});
|
||||
it("shows an unlimited quota without a full bar", function () {
|
||||
expect(html).to.match(/quota-fill" v-if="!quota\[q\?\.key\]\.unlimited"/);
|
||||
const quota = fs.readFileSync(path.join(__dirname, "../public/partials/quotaUsage.htm"), "utf8");
|
||||
expect(quota).to.match(/quota-fill" v-if="!quota\[q\?\.key\]\.unlimited"/);
|
||||
expect(html).to.not.match(/bg-success|bg-warning|bg-danger/);
|
||||
});
|
||||
it("right-aligns the Views column and marks it sortable", function () {
|
||||
|
||||
@@ -9,6 +9,7 @@ function harness(date) {
|
||||
const defs = {}, routes = {}, timers = new Map();
|
||||
let timerId = 0;
|
||||
const context = { reactive: value => value, console, Map, Set, Date: date || Date,
|
||||
formDraft: state => ({ restore: () => false, clear() {}, discard() {}, checkpoint: () => ({ terms: state.terms, options: state.options }), accept() {} }),
|
||||
navigator: { platform: "Linux" }, document: { location: { pathname: "/r/repo" }, addEventListener() {}, querySelector() {} },
|
||||
window: {}, Prism: { highlightAll() {} }, encodeURIComponent,
|
||||
encodePathForUrl: p => p.split("/").map(encodeURIComponent).join("/"),
|
||||
@@ -71,6 +72,11 @@ describe("frontend production regressions", function () {
|
||||
expect(h.scope.options.expirationDate.toISOString()).to.equal("2026-11-01T00:00:00.000Z");
|
||||
expect(h.scope.conference_data).not.to.equal(null);
|
||||
if (id === "repoId") {
|
||||
h.requests.find(r => r.url === "/api/repo/owner/repo/").resolve({ data: { repo: "repo", defaultBranch: "main" } });
|
||||
h.requests.find(r => r.url === "/api/repo/owner/repo/readme").resolve({ data: "README" });
|
||||
await h.flush();
|
||||
h.requests.find(r => r.url === "/api/repo/owner/repo/branches").resolve({ data: [{ name: "main", commit: "abcdef", readme: "README" }] });
|
||||
await h.flush();
|
||||
h.scope.anonymizeRepo({ target: {} });
|
||||
expect(h.requests.at(-1).body.options.update).to.equal(false);
|
||||
expect(h.requests.at(-1).url).to.equal("/api/repo/saved");
|
||||
@@ -163,9 +169,9 @@ describe("frontend production regressions", function () {
|
||||
it("translates profile save failures", async function () {
|
||||
const h = harness(); expect(h.defs.profileController.toString()).to.include("translate");
|
||||
const timeout = Object.assign(() => 0, { cancel() {} });
|
||||
h.defs.profileController(h.scope, h.http, key => Promise.resolve(key), timeout, { load: () => Promise.resolve({}) });
|
||||
h.defs.profileController(h.scope, h.http, key => Promise.resolve(key === "ERRORS.not_connected" ? "Please sign in again." : key), timeout, { load: () => Promise.resolve({}) });
|
||||
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("Please sign in again.");
|
||||
});
|
||||
describe("landing page features", function () {
|
||||
const home = fs.readFileSync(path.join(__dirname, "..", "public", "partials", "home.htm"), "utf8");
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const { expect } = require("chai");
|
||||
require("ts-node/register/transpile-only");
|
||||
const User = require("../src/core/User").default;
|
||||
const UserModel = require("../src/core/model/users/users.model").default;
|
||||
const { projectNameKey, saveProjectName } = require("../src/server/routes/project-names");
|
||||
|
||||
describe("private project names", () => {
|
||||
let updateOne, writes, user;
|
||||
beforeEach(() => {
|
||||
updateOne = UserModel.updateOne;
|
||||
writes = [];
|
||||
UserModel.updateOne = async (query, update) => { writes.push({ query, update }); };
|
||||
user = new User(new UserModel({ username: "owner" }));
|
||||
user.getRepositories = async () => [{ repoId: "artifact.v2" }];
|
||||
user.getPullRequests = async () => [{ pullRequestId: "artifact.v2" }];
|
||||
user.getGists = async () => [{ gistId: "artifact.v2" }];
|
||||
});
|
||||
afterEach(() => { UserModel.updateOne = updateOne; });
|
||||
|
||||
for (const type of ["repo", "pr", "gist"]) {
|
||||
it(`saves a ${type} name only on the current user's record`, async () => {
|
||||
const name = await saveProjectName(user, { type, id: "artifact.v2", name: " Reproduction package " });
|
||||
expect(name).to.equal("Reproduction package");
|
||||
expect(writes).to.have.length(1);
|
||||
expect(String(writes[0].query._id)).to.equal(String(user.model._id));
|
||||
expect(writes[0].update).to.deep.equal({ $set: { ["projectNames." + projectNameKey(type, "artifact.v2")]: name } });
|
||||
expect(user.model.projectNames.get(projectNameKey(type, "artifact.v2"))).to.equal(name);
|
||||
});
|
||||
}
|
||||
it("keeps names for different artifact types distinct", () => {
|
||||
expect(projectNameKey("repo", "same.id")).not.to.equal(projectNameKey("pr", "same.id"));
|
||||
expect(projectNameKey("repo", "same.id")).to.match(/^[a-f0-9]{64}$/);
|
||||
});
|
||||
it("removes a saved name when the field is cleared", async () => {
|
||||
await saveProjectName(user, { type: "repo", id: "artifact.v2", name: "Package" });
|
||||
await saveProjectName(user, { type: "repo", id: "artifact.v2", name: " " });
|
||||
expect(writes[1].update).to.deep.equal({ $unset: { ["projectNames." + projectNameKey("repo", "artifact.v2")]: "" } });
|
||||
expect(user.model.projectNames.has(projectNameKey("repo", "artifact.v2"))).to.equal(false);
|
||||
});
|
||||
it("rejects naming a project outside the user's accessible records", async () => {
|
||||
let error;
|
||||
try { await saveProjectName(user, { type: "repo", id: "someone-elses-project", name: "Mine" }); }
|
||||
catch (caught) { error = caught; }
|
||||
expect(error.httpStatus).to.equal(403);
|
||||
expect(writes).to.have.length(0);
|
||||
});
|
||||
it("rejects invalid names and object-shaped identifiers before writing", async () => {
|
||||
for (const input of [null, { type: ["repo"], id: "artifact.v2", name: "Bad" },
|
||||
{ type: "repo", id: { $ne: null }, name: "Bad" }, { type: "repo", id: "artifact.v2", name: "x".repeat(101) },
|
||||
{ type: "repo", id: "artifact.v2", name: "Two\nlines" },
|
||||
...[0, 31, 127].map(code => ({ type: "repo", id: "artifact.v2", name: "Name" + String.fromCharCode(code) }))]) {
|
||||
let error;
|
||||
try { await saveProjectName(user, input); } catch (caught) { error = caught; }
|
||||
expect(error.httpStatus).to.equal(400);
|
||||
}
|
||||
expect(writes).to.have.length(0);
|
||||
});
|
||||
it("does not change the in-memory name when persistence fails", async () => {
|
||||
UserModel.updateOne = async () => { throw Error("Database unavailable"); };
|
||||
let error;
|
||||
try { await saveProjectName(user, { type: "repo", id: "artifact.v2", name: "Package" }); }
|
||||
catch (caught) { error = caught; }
|
||||
expect(error.message).to.equal("Database unavailable");
|
||||
expect(user.model.projectNames).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
@@ -77,6 +77,336 @@ describe("Vue 3 UI", function () {
|
||||
let ui;
|
||||
afterEach(() => ui?.close());
|
||||
|
||||
const repositorySource = {
|
||||
"/api/user": { username: "owner" },
|
||||
"/api/user/default": { terms: ["Default Author"], options: {} },
|
||||
"/api/repo/owner/repo/": { defaultBranch: "main", repo: "repo", hasPage: false },
|
||||
"/api/repo/owner/repo/branches": [{ name: "main", commit: "abcdef123" }],
|
||||
"/api/repo/owner/repo/readme": "# Private Author",
|
||||
"/api/anonymize-preview": request => ({ content: request.payload.content.replaceAll("Private Author", "MASKED") }),
|
||||
};
|
||||
|
||||
it("keeps the current repository when earlier metadata and README responses arrive late", async () => {
|
||||
let finishMetadata, finishReadme;
|
||||
ui = await browser("/anonymize", {
|
||||
...repositorySource,
|
||||
"/api/repo/owner/old/": () => new Promise(resolve => { finishMetadata = () => resolve({ repo: "old", id: "old-id", defaultBranch: "old-main", hasPage: true }); }),
|
||||
"/api/repo/owner/old/readme": () => new Promise(resolve => { finishReadme = () => resolve("OLD PRIVATE README"); }),
|
||||
"/api/repo/owner/repo/": { repo: "repo", id: "new-id", defaultBranch: "main", hasPage: false },
|
||||
"/api/anonymize-preview": request => ({ content: request.payload.content }),
|
||||
});
|
||||
const state = ui.window.document.querySelector("#sourceUrl")._field.binding.state;
|
||||
state.sourceUrl = "https://github.com/owner/old";
|
||||
const oldLoad = state.urlSelected();
|
||||
await delay(10);
|
||||
state.sourceUrl = "https://github.com/owner/repo";
|
||||
await state.urlSelected();
|
||||
finishMetadata(); finishReadme(); await oldLoad;
|
||||
await delay(240);
|
||||
expect(state.details.repo).to.equal("repo");
|
||||
expect(state.repositoryID).to.equal("new-id");
|
||||
expect(state.source.branch).to.equal("main");
|
||||
expect(ui.window.document.querySelector(".anonymize-preview-body").textContent).to.include("Private Author");
|
||||
expect(ui.window.document.querySelector(".anonymize-preview-body").textContent).not.to.include("OLD PRIVATE README");
|
||||
expect(state.sourceLoading).to.equal(false);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("ignores old branches and source errors after changing repositories", async () => {
|
||||
let finishBranches;
|
||||
ui = await browser("/anonymize", {
|
||||
...repositorySource,
|
||||
"/api/repo/owner/old/": { repo: "old", defaultBranch: "old-main" },
|
||||
"/api/repo/owner/old/readme": "",
|
||||
"/api/repo/owner/old/branches": () => new Promise(resolve => { finishBranches = () => resolve({ __status: 404, body: { error: "repo_not_found" } }); }),
|
||||
});
|
||||
const state = ui.window.document.querySelector("#sourceUrl")._field.binding.state;
|
||||
state.sourceUrl = "https://github.com/owner/old"; const oldLoad = state.urlSelected();
|
||||
await delay(10);
|
||||
state.sourceUrl = "https://github.com/owner/repo"; await state.urlSelected();
|
||||
finishBranches(); await oldLoad;
|
||||
expect(state.branches.map(b => b.name)).to.deep.equal(["main"]);
|
||||
expect(state.anonymize.sourceUrl.invalid).to.equal(false);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("reloads a source when the default GitHub connection arrives after source loading begins", async () => {
|
||||
let finishConnections, finishOldMetadata;
|
||||
ui = await browser("/anonymize", {
|
||||
...repositorySource,
|
||||
"/github/connections": () => new Promise(resolve => { finishConnections = () => resolve({ appEnabled: true, appConnected: true }); }),
|
||||
"/api/repo/owner/repo/": request => request.url.searchParams.get("connection") === "github-app"
|
||||
? { repo: "repo", id: "app-id", defaultBranch: "main" }
|
||||
: new Promise(resolve => { finishOldMetadata = () => resolve({ repo: "old", id: "old-id", defaultBranch: "old-main" }); }),
|
||||
});
|
||||
const state = ui.window.document.querySelector("#sourceUrl")._field.binding.state;
|
||||
state.sourceUrl = "https://github.com/owner/repo";
|
||||
const oldLoad = state.urlSelected();
|
||||
await delay(10);
|
||||
finishConnections();
|
||||
await delay(50);
|
||||
finishOldMetadata(); await oldLoad;
|
||||
expect(state.githubConnection).to.equal("github-app");
|
||||
expect(state.repositoryID).to.equal("app-id");
|
||||
expect(state.source.branch).to.equal("main");
|
||||
expect(state.sourceLoading).to.equal(false);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
for (const kind of ["pr", "gist"]) {
|
||||
it(`ignores late ${kind} metadata after switching sources`, async () => {
|
||||
let finish;
|
||||
const oldPath = kind === "pr" ? "/api/pr/owner/old/42" : "/api/gist/source/abc123";
|
||||
ui = await browser("/anonymize", { ...repositorySource, [oldPath]: () => new Promise(resolve => { finish = () => resolve(kind === "pr" ? { pullRequest: { title: "OLD PR" } } : { gist: { description: "OLD GIST" } }); }) });
|
||||
const state = ui.window.document.querySelector("#sourceUrl")._field.binding.state;
|
||||
state.sourceUrl = kind === "pr" ? "https://github.com/owner/old/pull/42" : "https://gist.github.com/abc123";
|
||||
const oldLoad = state.urlSelected(); await delay(10);
|
||||
state.sourceUrl = "https://github.com/owner/repo"; await state.urlSelected();
|
||||
finish(); await oldLoad;
|
||||
expect(state.details.repo).to.equal("repo");
|
||||
expect(state.detectedType).to.equal("repo");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
}
|
||||
|
||||
it("preserves and restores edits made while defaults are saving", async () => {
|
||||
let finishSave;
|
||||
ui = await browser("/profile", { ...repositorySource, "/api/user/default": request => request.method === "POST"
|
||||
? new Promise(resolve => { finishSave = () => resolve({}); })
|
||||
: { terms: ["Original"], options: {} } });
|
||||
await ui.input("#terms", "Submitted terms");
|
||||
ui.window.document.querySelector("#save").click(); await delay(10);
|
||||
await ui.input("#terms", "Newer unsaved terms"); await delay(270);
|
||||
finishSave(); await delay(30);
|
||||
expect(ui.window.document.querySelector("#terms").value).to.equal("Newer unsaved terms");
|
||||
expect(JSON.parse(ui.window.sessionStorage.getItem("form-draft:profile")).values.terms).to.equal("Newer unsaved terms");
|
||||
expect(ui.window.document.querySelector("#save").textContent).not.to.equal("Saved");
|
||||
await ui.go("/dashboard"); await ui.go("/profile");
|
||||
expect(ui.window.document.querySelector("#terms").value).to.equal("Newer unsaved terms");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("discards a restored draft and cancels pending textarea edits", async () => {
|
||||
ui = await browser("/profile", repositorySource, { "form-draft:profile": { account: "owner", savedAt: Date.now(), values: { terms: "Restored draft", options: {} } } });
|
||||
await ui.input("#terms", "Pending edit");
|
||||
ui.window.document.querySelector(".draft-discard").click(); await delay(300);
|
||||
expect(ui.window.document.querySelector("#terms").value).to.equal("Default Author");
|
||||
expect(ui.window.sessionStorage.getItem("form-draft:profile")).to.equal(null);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("uses a readable fallback for unknown settings errors", async () => {
|
||||
ui = await browser("/profile", { ...repositorySource, "/api/user/default": request => request.method === "POST" ? { __status: 503, body: { error: "untranslated_failure" } } : { terms: [], options: {} } });
|
||||
ui.window.document.querySelector("#save").click(); await delay(30);
|
||||
expect(ui.window.document.querySelector('[role="alert"]').textContent).to.include("Unable to save your defaults");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("shows conference load failures and retries without a false empty state", async () => {
|
||||
let fail = true;
|
||||
ui = await browser("/conferences", { "/api/user": { username: "owner" }, "/api/conferences/": () => fail ? { __status: 503, body: { error: "unavailable" } } : [{ conferenceID: "ICSE26", name: "ICSE 2026", status: "ready" }] });
|
||||
expect(ui.window.document.body.textContent).to.include("Unable to load your conferences");
|
||||
expect(ui.window.document.body.textContent).not.to.include("You have not created a conference yet");
|
||||
fail = false; ui.window.document.querySelector(".conference-feedback button").click(); await delay(30);
|
||||
await ui.input("#search", " icse ");
|
||||
expect(ui.window.document.querySelector(".repo-name").textContent).to.equal("ICSE 2026");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("prevents duplicate conference saves and keeps a visible failure message", async () => {
|
||||
let finishSave;
|
||||
ui = await browser("/conference/new", { "/api/user": { username: "owner" }, "/api/conferences/": () => new Promise(resolve => { finishSave = () => resolve({ __status: 503, body: { error: "unavailable" } }); }) });
|
||||
await ui.input("#name", "Test venue"); await ui.input("#conferenceID", "TEST26");
|
||||
const button = ui.window.document.querySelector("#send"); button.click(); button.click(); await delay(10);
|
||||
expect(button.disabled).to.equal(true);
|
||||
expect(ui.requests.filter(r => r.method === "POST" && r.url.pathname === "/api/conferences/")).to.have.length(1);
|
||||
finishSave(); await delay(30);
|
||||
expect(button.disabled).to.equal(false);
|
||||
expect(ui.window.document.querySelector('[role="alert"]').textContent).to.include("Unable to save the conference");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("keeps an enabled PR preview selected and supports arrow-key navigation", async () => {
|
||||
ui = await browser("/pull-request-anonymize/saved", {
|
||||
...repositorySource,
|
||||
"/api/pr/saved": { source: { repositoryFullName: "owner/repo", pullRequestId: 42 }, options: { terms: [] } },
|
||||
"/api/pr/owner/repo/42": { pullRequest: { title: "Example", diff: "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-old\n+new", comments: [{ author: "Reviewer", body: "Visible comment" }] } },
|
||||
"/api/anonymize-preview": request => ({ contents: request.payload.contents }),
|
||||
});
|
||||
await delay(250);
|
||||
ui.window.document.querySelector("#diff").click(); await delay(240);
|
||||
expect(ui.window.document.querySelector("#preview-pr-comments-tab").getAttribute("aria-selected")).to.equal("true");
|
||||
expect(ui.window.document.querySelector("#preview-pr-comments-panel").textContent).to.include("Visible comment");
|
||||
ui.window.document.querySelector("#diff").click(); await delay(240);
|
||||
ui.window.document.querySelector("#preview-pr-comments-tab").dispatchEvent(new ui.window.KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true })); await delay(10);
|
||||
expect(ui.window.document.activeElement.id).to.equal("preview-pr-diff-tab");
|
||||
expect(ui.window.document.querySelector("#preview-pr-diff-tab").getAttribute("aria-selected")).to.equal("true");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("keeps focus during partial URL entry and transfers it after a valid URL", async () => {
|
||||
ui = await browser("/anonymize", repositorySource);
|
||||
const landing = ui.window.document.querySelector("#sourceUrl-landing");
|
||||
landing.focus();
|
||||
await ui.input("#sourceUrl-landing", "h");
|
||||
await delay(1050);
|
||||
expect(ui.window.document.activeElement).to.equal(landing);
|
||||
expect(ui.window.document.querySelector(".anonymize-workspace").style.display).to.equal("none");
|
||||
await ui.input("#sourceUrl-landing", "https://github.com/owner/repo");
|
||||
landing.dispatchEvent(new ui.window.Event("blur"));
|
||||
await delay(40);
|
||||
expect(ui.window.document.activeElement.id).to.equal("sourceUrl");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("keeps custom redactions, identifiers and commits when correcting the same source URL", async () => {
|
||||
ui = await browser("/anonymize", repositorySource);
|
||||
const source = await ui.input("#sourceUrl", "https://github.com/owner/repo");
|
||||
source.dispatchEvent(new ui.window.Event("blur"));
|
||||
await delay(40);
|
||||
await ui.input("#terms", "Private Author");
|
||||
await delay(270);
|
||||
const id = await ui.input("#repoId", "chosen-id");
|
||||
id.dispatchEvent(new ui.window.Event("blur"));
|
||||
await ui.input("#commit", "123456abcdef");
|
||||
await ui.input("#sourceUrl", "https://github.com/owner/repo/");
|
||||
source.dispatchEvent(new ui.window.Event("blur"));
|
||||
await delay(50);
|
||||
expect(ui.window.document.querySelector("#terms").value).to.equal("Private Author");
|
||||
expect(ui.window.document.querySelector("#repoId").value).to.equal("chosen-id");
|
||||
expect(ui.window.document.querySelector("#commit").value).to.equal("123456abcdef");
|
||||
expect(ui.window.document.querySelector(".anonymize-form-col").textContent).not.to.include("Switch the branch above");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
for (const kind of ["repo", "pr", "gist"]) {
|
||||
it(`hides original ${kind} content after a preview failure and supports retry`, async () => {
|
||||
let fail = true;
|
||||
const route = { repo: "/anonymize/saved", pr: "/pull-request-anonymize/saved", gist: "/gist-anonymize/saved" }[kind];
|
||||
const fixtures = {
|
||||
...repositorySource,
|
||||
"/api/repo/saved": { source: { fullName: "owner/repo", branch: "main", commit: "abcdef123" }, options: { terms: ["Private Author"] } },
|
||||
"/api/pr/saved": { source: { repositoryFullName: "owner/repo", pullRequestId: 42 }, options: { terms: ["Private Author"] } },
|
||||
"/api/pr/owner/repo/42": { pullRequest: { title: "Private Author", body: "Private Author body", comments: [] } },
|
||||
"/api/gist/saved": { source: { gistId: "abc123" }, options: { terms: ["Private Author"] } },
|
||||
"/api/gist/source/abc123": { gist: { description: "Private Author", files: [{ filename: "code.txt", content: "Private Author" }], comments: [] } },
|
||||
"/api/anonymize-preview": request => {
|
||||
if (fail) return { __status: 503, body: { error: "unavailable" } };
|
||||
const mask = value => value.replaceAll("Private Author", "MASKED");
|
||||
return request.payload.contents ? { contents: request.payload.contents.map(mask) } : { content: mask(request.payload.content) };
|
||||
},
|
||||
};
|
||||
ui = await browser(route, fixtures);
|
||||
await delay(280);
|
||||
const preview = () => ui.window.document.querySelector(".anonymize-preview-col");
|
||||
expect(preview().textContent).to.include("Unable to generate");
|
||||
expect(preview().textContent).not.to.include("Private Author");
|
||||
expect(preview().textContent).not.to.include("redactions applied");
|
||||
fail = false;
|
||||
[...preview().querySelectorAll("button")].find(b => b.textContent === "Retry preview").click();
|
||||
await delay(260);
|
||||
expect(preview().textContent).to.include("MASKED");
|
||||
expect(preview().textContent).to.include("redactions applied");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
}
|
||||
|
||||
it("ignores an old preview response while new redactions are waiting for the debounce", async () => {
|
||||
let finishOld;
|
||||
let count = 0;
|
||||
ui = await browser("/pull-request-anonymize/saved", {
|
||||
"/api/user": { username: "owner" },
|
||||
"/api/pr/saved": { source: { repositoryFullName: "owner/repo", pullRequestId: 42 }, options: { terms: ["Alice"] } },
|
||||
"/api/pr/owner/repo/42": { pullRequest: { title: "Alice", comments: [] } },
|
||||
"/api/anonymize-preview": () => ++count === 1
|
||||
? new Promise(resolve => { finishOld = () => resolve({ contents: ["OLD PREVIEW"] }); })
|
||||
: { contents: ["LATEST PREVIEW"] },
|
||||
});
|
||||
await delay(240);
|
||||
await ui.input("#terms", "Bob");
|
||||
await delay(270);
|
||||
finishOld();
|
||||
await delay(20);
|
||||
const preview = () => ui.window.document.querySelector(".anonymize-preview-col").textContent;
|
||||
expect(preview()).not.to.include("OLD PREVIEW");
|
||||
expect(preview()).to.include("Updating");
|
||||
await delay(230);
|
||||
expect(preview()).to.include("LATEST PREVIEW");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("submits billing details for a new paid conference", async () => {
|
||||
ui = await browser("/conference/new", {
|
||||
"/api/user": { username: "owner" },
|
||||
"/api/conferences/plans": [{ id: "free_conference", name: "Free", pricePerRepo: 0 }, { id: "premium_conference", name: "Premium", pricePerRepo: 0.5 }],
|
||||
});
|
||||
await ui.input("#name", "Review venue");
|
||||
await ui.input("#conferenceID", "REVIEW26");
|
||||
ui.window.document.querySelector('input[value="premium_conference"]').click();
|
||||
await delay(20);
|
||||
for (const [id, value] of Object.entries({ billing_name: "Jane Smith", email: "jane@example.org", inputAddress: "12 Main St", city: "Paris", country: "France", zip: "75001" })) await ui.input("#" + id, value);
|
||||
expect(ui.window.document.querySelector("#billing_name").value).to.equal("Jane Smith");
|
||||
ui.window.document.querySelector("#send").click();
|
||||
await delay(30);
|
||||
const request = ui.requests.find(r => r.method === "POST" && r.url.pathname === "/api/conferences/");
|
||||
expect(request?.payload.billing).to.include({ name: "Jane Smith", email: "jane@example.org", city: "Paris" });
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("opens FAQ fragments on direct load and subsequent navigation", async () => {
|
||||
ui = await browser("/faq#formats");
|
||||
await delay(370);
|
||||
expect(ui.window.document.querySelector("#formats").classList.contains("show")).to.equal(true);
|
||||
expect(ui.window.document.activeElement.id).to.equal("headingFormats");
|
||||
await ui.go("/faq#permissions");
|
||||
await delay(370);
|
||||
expect(ui.window.document.querySelector("#permissions").classList.contains("show")).to.equal(true);
|
||||
expect(ui.window.document.activeElement.id).to.equal("headingPermissions");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
for (const page of ["profile", "anonymize"]) {
|
||||
it(`restores unsaved ${page} redactions after navigation`, async () => {
|
||||
ui = await browser("/" + page, repositorySource);
|
||||
await ui.input("#terms", "Unsaved Author");
|
||||
// Leaving must flush the pending textarea debounce into the draft.
|
||||
await ui.go("/dashboard");
|
||||
await ui.go("/" + page);
|
||||
await delay(40);
|
||||
expect(ui.window.document.querySelector("#terms").value).to.equal("Unsaved Author");
|
||||
expect(ui.window.document.querySelector(".draft-status").textContent).to.include("restored");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
}
|
||||
|
||||
it("does not restore another account's draft", async () => {
|
||||
ui = await browser("/profile", { "/api/user": { username: "owner" } }, {
|
||||
"form-draft:profile": { account: "someone-else", savedAt: Date.now(), values: { terms: "Other account", options: {} } },
|
||||
});
|
||||
expect(ui.window.document.querySelector("#terms").value).not.to.equal("Other account");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("copies the anonymous URL and reports a clipboard failure without claiming success", async () => {
|
||||
ui = await browser("/dashboard", {
|
||||
"/api/user": { username: "owner" },
|
||||
"/api/user/anonymized_repositories": [{ repoId: "share-me", status: "ready", source: { fullName: "owner/private" }, options: {} }],
|
||||
});
|
||||
const copied = [];
|
||||
ui.window.navigator.clipboard = { writeText: async value => copied.push(value) };
|
||||
const button = [...ui.window.document.querySelectorAll("button")].find(b => b.textContent.includes("Copy anonymous link"));
|
||||
button.click();
|
||||
await delay(20);
|
||||
expect(copied).to.deep.equal(["http://localhost/r/share-me/"]);
|
||||
expect(ui.app.state.toasts[0].title).to.equal("Link copied");
|
||||
ui.window.navigator.clipboard.writeText = async () => { throw Error("Denied"); };
|
||||
button.click();
|
||||
await delay(20);
|
||||
expect(ui.app.state.toasts.at(-1).title).to.equal("Copy this anonymous link");
|
||||
expect(ui.app.state.toasts.at(-1).body).to.equal("http://localhost/r/share-me/");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
for (const path of [
|
||||
"/r/submission-artifact-604F/",
|
||||
"/r/submission-artifact-604F",
|
||||
@@ -550,6 +880,41 @@ describe("Vue 3 UI", function () {
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("shows quota limits accessibly and dismisses usage with Escape or an outside click", async () => {
|
||||
ui = await browser("/dashboard", {
|
||||
"/api/user": { username: "tester" },
|
||||
"/api/user/quota": {
|
||||
repository: { used: 21, total: 20 },
|
||||
storage: { used: 0, total: 1024 },
|
||||
file: { used: 1381, total: 0 },
|
||||
},
|
||||
});
|
||||
const doc = ui.window.document;
|
||||
const details = doc.querySelector(".dashboard-quota");
|
||||
const summary = details.querySelector("summary");
|
||||
summary.click();
|
||||
await delay(20);
|
||||
expect(details.open).to.equal(true);
|
||||
const bars = [...details.querySelectorAll('[role="progressbar"]')];
|
||||
expect(bars).to.have.length(2);
|
||||
expect(bars[0].getAttribute("aria-valuenow")).to.equal("20");
|
||||
expect(bars[0].getAttribute("aria-valuemax")).to.equal("20");
|
||||
expect(bars[1].getAttribute("aria-valuenow")).to.equal("0");
|
||||
expect(details.textContent).to.include("Limit exceeded").and.include("Unlimited");
|
||||
doc.querySelector('[aria-label="Close usage"]').focus();
|
||||
doc.activeElement.dispatchEvent(new ui.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||
await delay(20);
|
||||
expect(details.open).to.equal(false);
|
||||
expect(doc.activeElement).to.equal(summary);
|
||||
summary.click();
|
||||
await delay(20);
|
||||
doc.querySelector("#search").click();
|
||||
await delay(20);
|
||||
expect(details.open).to.equal(false);
|
||||
expect(summary.getAttribute("aria-expanded")).to.equal("false");
|
||||
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" },
|
||||
@@ -570,6 +935,93 @@ describe("Vue 3 UI", function () {
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("finds errors and stalled downloads without treating fresh queued projects as failures", async () => {
|
||||
ui = await browser("/dashboard", {
|
||||
"/api/user": { username: "tester" },
|
||||
"/api/user/anonymized_repositories": [
|
||||
{ repoId: "ready", status: "ready", source: { fullName: "owner/ready" } },
|
||||
{ repoId: "error", status: "error", statusMessage: "branch_not_found", source: { fullName: "owner/error" } },
|
||||
{ repoId: "fresh", status: "queue", anonymizeDate: new Date().toISOString(), source: { fullName: "owner/fresh" } },
|
||||
{ repoId: "stalled", status: "download", anonymizeDate: "2000-01-01T00:00:00Z", source: { fullName: "owner/stalled" } },
|
||||
],
|
||||
});
|
||||
const state = ui.window.document.querySelector("#search")._field.binding.state;
|
||||
expect(state.attentionCount()).to.equal(2);
|
||||
state.filters.status.error = false;
|
||||
state.setProjectView("attention");
|
||||
await delay(10);
|
||||
expect(state.filteredItems.map(item => item._id).sort()).to.deep.equal(["error", "stalled"]);
|
||||
expect(ui.window.document.querySelector('[href="/anonymize/error"]').textContent).to.equal("Fix source");
|
||||
expect(ui.window.document.querySelector(".filter-chip")?.textContent).not.to.include("Error hidden");
|
||||
await ui.input("#search", "stalled");
|
||||
expect(state.filteredItems).to.have.length(1);
|
||||
state.clearFilters(); await delay(10);
|
||||
expect(state.filteredItems).to.have.length(4);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("saves private project names and can search them after returning to the dashboard", async () => {
|
||||
const gist = { gistId: "g-demo", status: "ready", source: { gistId: "a1b2c3d4e5" } };
|
||||
let payload;
|
||||
ui = await browser("/dashboard", {
|
||||
"/api/user": { username: "tester" },
|
||||
"/api/user/anonymized_gists": () => [gist],
|
||||
"/api/user/project-name": request => { payload = request.payload; gist.projectName = payload.name; return { name: payload.name }; },
|
||||
});
|
||||
const state = ui.window.document.querySelector("#search")._field.binding.state;
|
||||
expect(state.items[0]._label).to.equal("Gist g-demo");
|
||||
await state.showProjectDetails(state.items[0]);
|
||||
await ui.input("#project-name-gist-g-demo", "Training utilities");
|
||||
ui.window.document.querySelector(".project-name-form").dispatchEvent(new ui.window.Event("submit", { bubbles: true, cancelable: true }));
|
||||
await delay(20);
|
||||
expect(payload).to.deep.equal({ type: "gist", id: "g-demo", name: "Training utilities" });
|
||||
expect(ui.window.document.querySelector(".repo-name").textContent).to.equal("Training utilities");
|
||||
await ui.go("/faq"); await ui.go("/dashboard");
|
||||
await ui.input("#search", " training ");
|
||||
expect(ui.window.document.querySelectorAll(".paper-table-row:not(.paper-table-skeleton)")).to.have.length(1);
|
||||
expect(ui.window.document.querySelector(".repo-name").textContent).to.equal("Training utilities");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("keeps the current project name visible when saving a replacement fails", async () => {
|
||||
let complete;
|
||||
ui = await browser("/dashboard", {
|
||||
"/api/user": { username: "tester" },
|
||||
"/api/user/anonymized_repositories": [{ repoId: "package", status: "ready", projectName: "Saved package", source: { fullName: "owner/package" } }],
|
||||
"/api/user/project-name": () => new Promise(resolve => { complete = () => resolve({ __status: 503, body: { error: "unavailable" } }); }),
|
||||
});
|
||||
const state = ui.window.document.querySelector("#search")._field.binding.state;
|
||||
const item = state.items[0];
|
||||
await state.showProjectDetails(item);
|
||||
state.projectName = "Replacement";
|
||||
const pending = state.saveProjectName(item);
|
||||
await state.saveProjectName(item);
|
||||
expect(ui.requests.filter(r => r.url.pathname === "/api/user/project-name")).to.have.length(1);
|
||||
complete(); await pending; await delay(10);
|
||||
expect(item._label).to.equal("Saved package");
|
||||
expect(ui.window.document.querySelector(".project-name-error").textContent).to.include("could not be saved");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("shows full project details as text and restores keyboard focus when closed", async () => {
|
||||
const source = "owner/" + "very-long-project-name-".repeat(5);
|
||||
ui = await browser("/dashboard", {
|
||||
"/api/user": { username: "tester" },
|
||||
"/api/user/anonymized_repositories": [{ repoId: "package", status: "ready", projectName: '<img src=x onerror="alert(1)">', source: { fullName: source, commit: "abcdef1234567890" }, conference: "A long conference name" }],
|
||||
});
|
||||
const state = ui.window.document.querySelector("#search")._field.binding.state;
|
||||
await state.showProjectDetails(state.items[0]);
|
||||
const panel = ui.window.document.querySelector(".project-details");
|
||||
expect(panel.textContent).to.include(source).and.include("abcdef1234567890").and.include("A long conference name");
|
||||
expect(ui.window.document.querySelector(".repo-name img")).to.equal(null);
|
||||
expect(ui.window.document.activeElement).to.equal(panel);
|
||||
panel.dispatchEvent(new ui.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||
await delay(10);
|
||||
expect(ui.window.document.querySelector(".project-details")).to.equal(null);
|
||||
expect(ui.window.document.activeElement.getAttribute("data-project-actions")).to.equal("repo%3Apackage");
|
||||
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"] } },
|
||||
|
||||