mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 08:17:26 +01:00
The "What reviewers will see" window now has a sidebar like the explorer's instead of the one-row file strip: folders first, then files, with the explorer's tree rows, icons and teal selection on the README. Folders expand from the loaded file list. The sidebar is 24% of the window (180-240px), tinted full height, and scrolls apart from the document. Phones hide it and show the explorer's "Files" button in the file bar. Loading shows skeleton rows; without a file list the sidebar shows the README only. Removes the strip fitter and the dead rail/strip CSS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
689 lines
72 KiB
HTML
689 lines
72 KiB
HTML
<div class="anonymize-page h-100" :class="{'mobile-preview-active': mobilePane === 'preview', 'preview-expanded': previewExpanded}">
|
||
<!-- ===== STATE 1: No URL — centered input ===== -->
|
||
<div class="anonymize-landing" v-show="!workspaceOpen && !isUpdate">
|
||
<div class="anonymize-landing-inner">
|
||
<header class="page-head">
|
||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">New anonymization</span></div>
|
||
<div class="page-head-main"><div>
|
||
<h1 class="paper-page-title">New anonymization</h1>
|
||
<p class="paper-page-lede">Paste a repository, pull request, or gist URL, or pick one of your repositories. You’ll choose the terms to redact next.</p>
|
||
</div></div>
|
||
<ol class="task-trail flow-trail" aria-label="Anonymization steps"><li aria-current="step">1. Repository</li><li>2. Review and hosting</li><li>3. Review link</li></ol>
|
||
</header>
|
||
<aside class="task-summary landing-next" aria-labelledby="landing-next-title">
|
||
<h2 id="landing-next-title">What happens next</h2>
|
||
<ol>
|
||
<li><b>Choose terms to redact.</b> Names, affiliations, and emails are replaced in every text file.</li>
|
||
<li><b>Pick hosting and expiry.</b> Decide what reviewers may see and when the link stops working.</li>
|
||
<li><b>Share the review link.</b> Reviewers open it without an account.</li>
|
||
</ol>
|
||
</aside>
|
||
<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')"><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' ? githubConnections?.gistConnection === 'github-app' : githubConnection === 'github-app') ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||
</aside>
|
||
</div>
|
||
<div class="landing-url-row"><input id="sourceUrl-landing" onblur="this.scrollLeft=0" 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() } }"><button type="button" class="btn btn-primary landing-continue" :aria-disabled="!sourceUrl?.trim() ? 'true' : null" @click="sourceUrl?.trim() && urlSelected()">Continue</button></div>
|
||
</div>
|
||
<p class="text-danger" role="alert" v-if="sourceUrlError">{{ sourceUrlError }}</p>
|
||
<small class="form-text landing-help" style="color: var(--ink-muted);">
|
||
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
|
||
</small>
|
||
<section class="repo-picker" aria-labelledby="repo-picker-title">
|
||
<div class="repo-picker-head">
|
||
<div class="repo-picker-title">
|
||
<h2 id="repo-picker-title">Your repositories</h2>
|
||
<span class="repo-picker-count" aria-live="polite" v-if="userReposStatus === 'ready' && userRepos.length > 0"><template v-if="userRepoFilter && filteredUserRepos().length !== userRepos.length">{{ filteredUserRepos().length }} of </template>{{ userRepos.length }} {{ userRepos.length === 1 ? 'repository' : 'repositories' }}<span class="repo-picker-more"><span class="repo-picker-more-sep"> · </span>scroll for more</span></span>
|
||
</div>
|
||
<input type="search" class="form-control repo-picker-filter" placeholder="Filter…" aria-label="Filter your repositories" autocomplete="off" v-model="userRepoFilter" v-if="userReposStatus === 'ready' && userRepos.length > 0">
|
||
</div>
|
||
<ul class="repo-picker-list" v-if="userReposStatus === 'loading' || userReposStatus === 'idle'" aria-busy="true" aria-label="Loading your repositories">
|
||
<template v-for="(i, index) in [1, 2, 3, 4]"><li class="repo-picker-row is-skeleton" aria-hidden="true"><span class="skeleton skeleton-line" :style="{ width: (34 + i * 9) + '%' }"></span></li></template>
|
||
</ul>
|
||
<ul class="repo-picker-list" v-if="userReposStatus === 'ready' && filteredUserRepos().length > 0">
|
||
<template v-for="(repo, index) in filteredUserRepos()" :key="repo.id || repo.fullName"><li class="repo-picker-row">
|
||
<svg class="project-type-svg" aria-hidden="true"><use href="#project-type-repo"></use></svg>
|
||
<span class="repo-picker-name" :title="repo.fullName"><span class="repo-picker-owner">{{ repo.fullName.split('/')[0] }}<span class="repo-picker-slash">/</span></span><span class="repo-picker-repo">{{ repo.fullName.split('/').slice(1).join('/') }}</span></span>
|
||
<button type="button" class="btn btn-outline-ink btn-sm repo-picker-select" @click="selectUserRepo(repo)" :aria-label="'Select ' + repo.fullName">Select</button>
|
||
</li></template>
|
||
</ul>
|
||
<p class="repo-picker-state" v-if="userReposStatus === 'ready' && userRepos.length > 0 && filteredUserRepos().length === 0">No repository matches “{{ userRepoFilter }}”.</p>
|
||
<p class="repo-picker-state" v-if="userReposStatus === 'ready' && userRepos.length === 0">No repositories found for your GitHub account. Paste a URL above, or grant access below.</p>
|
||
<p class="repo-picker-state" role="alert" v-if="userReposStatus === 'error'">Your repositories could not be loaded. <button type="button" class="task-text-button" @click="loadUserRepos(true)">Try again</button></p>
|
||
<aside class="repo-access-details" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected">
|
||
<div class="repo-access-main">
|
||
<div class="repo-access-controls">
|
||
<span class="access-q" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub 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'">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||
</div>
|
||
</div>
|
||
<div class="repo-access-note"><span v-if="error && detectedType !== 'gist' && githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage GitHub access to select this repository, then return here. Organization access may require administrator approval.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Anonymized content is public, even from private repositories.</span></div>
|
||
</aside>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== -->
|
||
<div class="anonymize-workspace" v-show="workspaceOpen || isUpdate">
|
||
<header class="anonymize-topbar">
|
||
<div class="anonymize-topbar-inner">
|
||
<div class="paper-crumbs"><a href="/dashboard">My work</a> / <span class="here">{{ isUpdate ? 'Edit anonymization' : 'New anonymization' }}</span></div>
|
||
<div class="anonymize-topbar-head">
|
||
<h1 class="paper-page-title anonymize-topbar-title">
|
||
<span v-if="!isUpdate">New anonymization</span>
|
||
<span v-if="isUpdate">Edit anonymization</span>
|
||
</h1>
|
||
<span class="type-badge" v-show="detectedType" :class="{'type-repo': detectedType === 'repo', 'type-pr': detectedType === 'pr', 'type-gist': detectedType === 'gist'}">
|
||
{{ detectedType === 'repo' ? 'Repository' : detectedType === 'pr' ? 'Pull request' : 'Gist' }}
|
||
</span>
|
||
</div>
|
||
<p class="paper-page-lede anonymize-topbar-lede">{{ isUpdate ? 'Changes apply to the existing anonymous link; reviewers keep the same URL.' : 'Choose the terms to redact, what reviewers see, and when the link expires.' }}</p>
|
||
<ol class="task-trail flow-trail" aria-label="Anonymization steps" v-if="!isUpdate"><li>1. Repository</li><li aria-current="step">2. Review and hosting</li><li>3. Review link</li></ol>
|
||
</div>
|
||
</header>
|
||
|
||
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' && gistAppRequired">
|
||
<p>Reconnect your GitHub account to read this gist. Your draft will be restored when you return.</p>
|
||
<button type="button" class="btn btn-ink" @click="connectGistApp()">Connect GitHub App to access gists</button>
|
||
</aside>
|
||
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' && gistOAuthRequired">
|
||
<p>Connect GitHub OAuth to access gists. Your account and existing anonymizations stay the same, and your draft will be restored when you return.</p>
|
||
<p>GitHub's legacy OAuth connection includes write access to private repositories.</p>
|
||
<button type="button" class="btn btn-ink" @click="connectGistOAuth()" v-if="githubConnections?.oauthEnabled">Connect GitHub OAuth to access gists</button>
|
||
<p v-else>GitHub OAuth is currently unavailable. Please try again later.</p>
|
||
</aside>
|
||
|
||
<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')"><span class="tab-label">Settings</span></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')"><span class="tab-label">Reviewer preview</span></button>
|
||
</nav>
|
||
<div class="anonymize-split">
|
||
<!-- Form column (left) -->
|
||
<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"><div class="anonymize-fields">
|
||
|
||
<section class="paper-settings-section">
|
||
<h2 class="paper-section-eyebrow">Source & access</h2>
|
||
|
||
<div class="form-group">
|
||
<div class="source-url-label-row">
|
||
<label class="paper-field-label" for="sourceUrl">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')"><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' ? githubConnections?.gistConnection === 'github-app' : githubConnection === 'github-app') ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
|
||
</aside>
|
||
</div>
|
||
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" onblur="this.scrollLeft=0" :disabled="isUpdate && detectedType !== 'repo'" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value => { sourceUrl = value }, value: sourceUrl, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }" :class="{'is-invalid': anonymize?.sourceUrl?.invalid}">
|
||
<p class="source-summary" v-if="sourceSummary() && !anonymize?.sourceUrl?.invalid"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg>{{ sourceSummary() }}</p>
|
||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.github">
|
||
Please provide a valid GitHub URL.
|
||
</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.access">
|
||
Not accessible. The organization may restrict access.
|
||
</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.missing">
|
||
Does not exist or is not accessible.
|
||
</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.used">
|
||
Already anonymized.
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="repo-access-details" aria-label="Previous repository configuration" v-if="!isUpdate && previousConfigurations.length">
|
||
<p role="status">You have already anonymized this repository. Use the existing configuration to keep its anonymous link and avoid creating another repository.</p>
|
||
<div class="form-group" v-if="previousConfigurations.length > 1">
|
||
<label class="paper-field-label" for="previousConfiguration">Previous anonymization</label>
|
||
<select id="previousConfiguration" class="form-control" v-model="previousConfigurationId">
|
||
<option v-for="previous in previousConfigurations" :key="previous.repoId" :value="previous.repoId">{{ previous.repoId }}</option>
|
||
</select>
|
||
</div>
|
||
<p v-else>{{ previousConfigurationId }}</p>
|
||
<p>You can review and update its settings before saving.</p>
|
||
<div class="repo-access-controls">
|
||
<a class="btn btn-ink" :href="existingConfigurationUrl()">Use existing configuration</a>
|
||
<button type="button" class="btn btn-outline-ink" @click="previousConfigurations = []">Create a separate one</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside class="repo-access-details" aria-label="GitHub access" v-if="sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected">
|
||
<div class="repo-access-main">
|
||
<div class="repo-access-controls">
|
||
<span class="access-q" v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details" @click="grantGitHubAccess()">Manage GitHub 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'">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
|
||
</div>
|
||
</div>
|
||
<div class="repo-access-note"><span v-if="error && detectedType !== 'gist' && githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage GitHub access to select this repository, then return here. Organization access may require administrator approval.</span><span v-if="detectedType !== 'gist' && githubConnection !== 'oauth' && !details">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>
|
||
|
||
<details class="repair-disclosure" v-if="isUpdate && detectedType === 'repo' && role === 'owner' && sourceUrl === 'https://github.com/' + _originalFullName" :open="!!(error || accessRepairMessage || reconnectRepositoryId)">
|
||
<summary>Connection problems?</summary>
|
||
<div class="repair-step" v-if="accessRepairAllowed && githubConnections?.appEnabled && githubConnection === 'github-app' && sourceUrl === 'https://github.com/' + _originalFullName">
|
||
<p>If this repository became private or its App installation changed, repair its access here. This keeps the same anonymous URL and published commit.</p>
|
||
<button type="button" class="btn btn-outline-ink" :disabled="repairingGitHubAccess" @click="repairGitHubAccess()">Repair GitHub access</button>
|
||
<button type="button" class="btn btn-outline-ink" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
||
<p v-if="accessRepairMessage" role="status">{{ accessRepairMessage }}</p>
|
||
</div>
|
||
|
||
<div class="repair-step">
|
||
<p>If you deleted and recreated this repository, reconnect it before choosing a branch.</p>
|
||
<button type="button" class="btn btn-outline-ink" @click="reconnectSource()">Reconnect source repository</button>
|
||
<p v-if="reconnectRepositoryId" role="status">Source reloaded. Review the branch, commit, and anonymization preview, then save to replace the published snapshot.</p>
|
||
</div>
|
||
</details>
|
||
|
||
<div class="form-grid-2" v-if="detectedType === 'repo'">
|
||
<div class="form-group">
|
||
<label class="paper-field-label" for="branch">Branch</label>
|
||
<div class="input-group">
|
||
<select class="form-control" id="branch" name="branch" v-field="{ state: viewState, set: value => { source.branch = value }, value: source?.branch, form: "anonymize", options: {} }">
|
||
<option v-for="(b, index) in branches" v-text="b?.name" :value="(b?.name)"></option>
|
||
</select>
|
||
|
||
<div class="input-group-append">
|
||
<button class="btn btn-icon refresh-branches" type="button" title="Refresh branches" @click="getBranches(true)" aria-label="Refresh branches">
|
||
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="paper-field-label" for="commit">Commit</label>
|
||
<input class="form-control" id="commit" :disabled="options.update" name="commit" pattern="[a-fA-Z0-9]{6,}" required v-field="{ state: viewState, set: value => { source.commit = value }, value: source?.commit, form: "anonymize", options: {} }" :class="{'is-invalid': anonymize?.commit?.invalid}">
|
||
<div class="invalid-feedback" v-show="anonymize?.commit?.errors?.pattern || anonymize?.commit?.errors?.required">
|
||
The commit SHA is not valid.
|
||
</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.commit?.errors?.exists">
|
||
This commit no longer exists in the repository. Click refresh to get the latest.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-check auto-update-check" v-show="detectedType" :class="{ 'is-paid': !liveUpdatesIncluded() }">
|
||
<input class="form-check-input" type="checkbox" id="update" name="update" aria-describedby="update-help" v-field="{ state: viewState, set: value => { options.update = value }, value: options?.update, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="update">Auto-update from GitHub<span class="paid-tag" v-if="!liveUpdatesIncluded()">Paid</span></label>
|
||
<small class="form-text text-muted" id="update-help" v-if="liveUpdatesIncluded()">Follows the latest version, at most once an hour.</small>
|
||
<small class="form-text text-muted auto-update-paid" id="update-help" v-else>Needs Live updates<template v-if="liveUpdatesPrice()"> · {{ liveUpdatesPrice() }}</template>, a Paper pack or an author subscription. <a href="#repository-hosting" @click.prevent="showHostingOptions()">Hosting options</a></small>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="paper-settings-section" v-show="detectedType">
|
||
<h2 class="paper-section-eyebrow">Anonymization</h2>
|
||
|
||
<div class="form-group" v-show="detectedType === 'repo'">
|
||
<label class="paper-field-label" for="repoId">Anonymized repository ID</label>
|
||
<input type="text" class="form-control" name="repoId" id="repoId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { repoId = value }, value: repoId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.repoId?.invalid}">
|
||
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>r/<wbr><span class="anon-url-id">{{ repoId }}</span></small>
|
||
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.format">ID can only contain letters and numbers.</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.used">{{ repoId }} is already used.</div>
|
||
</div>
|
||
|
||
<div class="form-group" v-show="detectedType === 'pr'">
|
||
<label class="paper-field-label" for="pullRequestId">Anonymized pull request ID</label>
|
||
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { pullRequestId = value }, value: pullRequestId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.pullRequestId?.invalid}">
|
||
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>pr/<wbr><span class="anon-url-id">{{ pullRequestId }}</span></small>
|
||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">ID can only contain letters and numbers.</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">{{ pullRequestId }} is already used.</div>
|
||
</div>
|
||
|
||
<div class="form-group" v-show="detectedType === 'gist'">
|
||
<label class="paper-field-label" for="gistId">Anonymized gist ID</label>
|
||
<input type="text" class="form-control" name="gistId" id="gistId" :disabled="isUpdate" v-field="{ state: viewState, set: value => { gistId = value }, value: gistId, form: "anonymize", options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' } }" :class="{'is-invalid': anonymize?.gistId?.invalid}">
|
||
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>gist/<wbr><span class="anon-url-id">{{ gistId }}</span></small>
|
||
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.format">ID can only contain letters and numbers.</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.used">{{ gistId }} is already used.</div>
|
||
</div>
|
||
|
||
|
||
|
||
|
||
<div class="form-group">
|
||
<label class="paper-field-label" for="terms">Terms to redact</label>
|
||
<textarea class="form-control terms-input" id="terms" name="terms" :rows="Math.min(14, Math.max(4, String(terms || "").split("\n").length + 1))" placeholder="Jane Smith MIT CSAIL jane.smith@example.org" v-field="{ state: viewState, set: value => { terms = value }, value: terms, form: "anonymize", options: { debounce: 250 } }" :class="{'is-invalid': anonymize?.terms?.invalid}"></textarea>
|
||
<small class="form-text text-muted">One term per line.</small><details class="redaction-syntax"><summary>Regex & replacement syntax</summary> <small class="form-text text-muted">Regex is supported. Replaced by <code>{{ site_options?.ANONYMIZATION_MASK }}-[N]</code>, or use <code>term=>replacement</code> to pick your own (e.g. <code>Anonymous=>ABC</code>).</small></details>
|
||
<div class="term-hint" v-for="hint in (termHints || []).slice(0, 3)" :key="hint.line">
|
||
<span>In <code>{{ hint.term }}</code>, <code>{{ hint.symbols.join(" ") }}</code> {{ hint.symbols.length > 1 ? "are regex symbols" : "is a regex symbol" }}, so it may not match the text as written.</span>
|
||
<button type="button" class="btn btn-outline-ink btn-sm" @click="matchTermAsWritten(hint.line)">Match as written</button>
|
||
</div>
|
||
<div class="term-hint-more" v-if="(termHints || []).length > 3">{{ termHints.length - 3 }} more {{ termHints.length - 3 === 1 ? "term uses" : "terms use" }} regex symbols.</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">Terms are in an invalid format.</div>
|
||
</div>
|
||
<div class="preserve-row" v-show="detectedType === 'repo'">
|
||
<div class="preserve-row-text">
|
||
<span class="paper-field-label" id="preserve-files-label">Unredacted files</span>
|
||
<span class="preserve-row-value">{{ preserveSummary() }}</span>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-ink btn-sm" aria-describedby="preserve-files-label" :disabled="!source?.commit" @click="openPreserveFiles()">{{ (options?.preserveFiles || []).length ? "Edit" : "Choose files" }}</button>
|
||
</div>
|
||
<dialog id="preserve-files-dialog" class="preserve-dialog" v-if="preserve?.open" aria-labelledby="preserve-files-title" @cancel.prevent="closePreserveFiles()">
|
||
<form @submit.prevent="savePreserveFiles()">
|
||
<h2 id="preserve-files-title">Leave files unredacted</h2>
|
||
<p class="preserve-intro">Checked files are shown exactly as written, for example a bibliography where redacted self-citations would reveal you. File names are still redacted.</p>
|
||
<input type="search" id="preserve-files-search" class="form-control" placeholder="Search files" aria-label="Search files" autocomplete="off" v-model="preserve.search">
|
||
<div class="preserve-tree">
|
||
<p class="preserve-status" v-if="preserve.loading">Loading files from GitHub…</p>
|
||
<p class="paper-inline-alert paper-inline-alert-error" role="alert" v-else-if="preserve.error">{{ preserve.error }}</p>
|
||
<template v-else>
|
||
<div class="preserve-item" v-for="(row, i) in preserveRows()" :key="row.kind + ':' + row.path" :style="{ paddingLeft: (row.depth * 18 + 6) + "px" }">
|
||
<button type="button" class="preserve-caret" v-if="row.kind === 'dir'" :aria-expanded="row.open ? "true" : "false"" :aria-label="(row.open ? "Collapse " : "Expand ") + row.label" @click="togglePreserveDir(row.path)">{{ row.open ? "▾" : "▸" }}</button>
|
||
<span class="preserve-caret-space" v-else></span>
|
||
<input type="checkbox" :id=""pf-" + i" :aria-label="row.kind === 'dir' ? "Leave folder " + row.path + " unredacted" : null" :checked="row.state === 'all'" :indeterminate.prop="row.state === 'some'" :disabled="row.locked" @change="row.kind === 'dir' ? togglePreserveFolder(row.path) : togglePreserveFile(row.path)">
|
||
<label :for="row.kind === 'file' ? "pf-" + i : null" class="preserve-name" @click="row.kind === 'dir' && togglePreserveDir(row.path)">{{ row.label }}</label>
|
||
<span class="preserve-note" v-if="row.note">{{ row.note }}</span>
|
||
</div>
|
||
<p class="preserve-status" v-if="!preserve.loading && !preserveRows().length">{{ preserve.search ? "No file matches your search." : "No text files in this commit." }}</p>
|
||
</template>
|
||
</div>
|
||
<p class="preserve-status" v-if="preserve.truncated">This repository has too many files to list them all. Use a pattern for files you cannot find.</p>
|
||
<details class="preserve-patterns" :open="preserve.patternsOpen">
|
||
<summary>Patterns</summary>
|
||
<textarea class="form-control" rows="3" aria-label="Patterns" placeholder="*.bib paper/appendix/**" v-model="preserve.patterns"></textarea>
|
||
<small class="form-text text-muted">One per line, as in .gitignore: <code>*.bib</code> matches in any folder; <code>/refs.bib</code> and <code>paper/refs.bib</code> match from the repository root; a folder name such as <code>drafts/</code> covers everything inside it; <code>!private.bib</code> excludes again.</small>
|
||
</details>
|
||
<div class="preserve-foot">
|
||
<span class="preserve-count" aria-live="polite">{{ preserve.loading || preserve.error ? "" : preserveCount() + (preserveCount() === 1 ? " file" : " files") + " will be shown as written" }}</span>
|
||
<span class="preserve-actions">
|
||
<button type="button" class="btn btn-outline-ink" @click="closePreserveFiles()">Cancel</button>
|
||
<button type="submit" class="btn btn-ink">Save</button>
|
||
</span>
|
||
</div>
|
||
</form>
|
||
</dialog>
|
||
</section>
|
||
|
||
<section class="paper-settings-section" v-show="detectedType">
|
||
<h2 class="paper-section-eyebrow">Display</h2>
|
||
|
||
<div v-show="detectedType === 'repo'">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value => { options.link = value }, value: options?.link, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="link">Keep links</label>
|
||
</div>
|
||
<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>
|
||
|
||
</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>
|
||
|
||
</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: {} }">
|
||
<label class="form-check-label" for="notebook">Display notebooks</label>
|
||
</div>
|
||
<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" v-if="!details?.hasPage">
|
||
{{ fmt?.translate('WARNINGS.page_not_enabled_on_repo') }}
|
||
</small>
|
||
<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>
|
||
</div>
|
||
|
||
<p class="form-text text-muted" v-if="detectedType === 'repo' && (options.image || options.pdf)">Images and PDFs are served unchanged. Review their contents and metadata before sharing. <a href="/faq#formats">Supported formats</a></p>
|
||
<div v-show="detectedType === 'gist'">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" v-field="{ state: viewState, set: value => { options.title = value }, value: options?.title, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="title-gist">Gist description</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" v-field="{ state: viewState, set: value => { options.content = value }, value: options?.content, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="content-gist">File contents</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" v-field="{ state: viewState, set: value => { options.comments = value }, value: options?.comments, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="comments-gist">Comments</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" v-field="{ state: viewState, set: value => { options.username = value }, value: options?.username, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="username-gist">Usernames</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" v-field="{ state: viewState, set: value => { options.date = value }, value: options?.date, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="date-gist">Dates</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" v-field="{ state: viewState, set: value => { options.origin = value }, value: options?.origin, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="origin-gist">Source gist ID</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-show="detectedType === 'pr'">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="title" name="title" v-field="{ state: viewState, set: value => { options.title = value }, value: options?.title, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="title">PR title</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="body" name="body" v-field="{ state: viewState, set: value => { options.body = value }, value: options?.body, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="body">PR body</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="diff" name="diff" v-field="{ state: viewState, set: value => { options.diff = value }, value: options?.diff, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="diff">Diff</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="comments" name="comments" v-field="{ state: viewState, set: value => { options.comments = value }, value: options?.comments, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="comments">Comments</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="username" name="username" v-field="{ state: viewState, set: value => { options.username = value }, value: options?.username, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="username">Usernames</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="date" name="date" v-field="{ state: viewState, set: value => { options.date = value }, value: options?.date, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="date">Dates</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="origin" name="origin" v-field="{ state: viewState, set: value => { options.origin = value }, value: options?.origin, form: "anonymize", options: {} }">
|
||
<label class="form-check-label" for="origin">Project name</label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="paper-settings-section hosting-settings" id="repository-hosting" v-show="detectedType" aria-labelledby="hosting-heading">
|
||
<h2 class="paper-section-eyebrow" id="hosting-heading">Hosting & expiration</h2>
|
||
<div class="form-group conference-field">
|
||
<label class="paper-field-label" for="conference">Conference code <span class="paper-optional">(optional)</span></label>
|
||
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value => { conference = value }, value: conference, form: "anonymize", options: { debounce: { default: 800, blur: 0 }, updateOn: 'default blur' } }" :class="{'is-invalid': anonymize?.conference?.invalid}">
|
||
<p class="conference-hosting" :class="{ 'is-covered': conferenceHosting() }" v-show="conference_data" role="status">
|
||
<a target="_blank" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a>
|
||
<template v-if="conferenceHosting()"> hosts this repository until {{ fmt?.date(conference_data?.endDate) }}, with automatic updates. You don't pay anything.</template>
|
||
<template v-else-if="billing?.enabled && detectedType === 'repo'"> runs until {{ fmt?.date(conference_data?.endDate) }}. It doesn't pay for hosting, so this repository uses your own allowance.</template>
|
||
<template v-else> runs until {{ fmt?.date(conference_data?.endDate) }}.</template>
|
||
</p>
|
||
<div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">The conference is not activated.</div>
|
||
<small class="form-text text-muted" v-show="!conference_data">Submitting to a conference? Its code applies the venue's settings, and a paid conference hosts the repository for you.</small>
|
||
</div>
|
||
<p class="hosting-usage" v-if="detectedType === 'repo' && billing?.enabled && !conferenceHosting()">
|
||
<span v-if="isUpdate && role !== 'owner'">Uses the owner's hosting</span>
|
||
<span v-else-if="hasAuthorHosting()">Your author subscription is active.</span>
|
||
<span v-else-if="hostingQuota?.repository">{{ hostingQuota.repository.used }} of {{ hostingQuota.repository.total }} active repositories · Free</span>
|
||
</p>
|
||
<p class="hosting-limit auto-update-note" role="status" v-if="detectedType === 'repo' && options?.update && !liveUpdatesIncluded()">Automatic updates need a paid option. On free hosting, the link keeps the version you create until you refresh it from My work. Once it exists, add Live updates<template v-if="liveUpdatesPrice()"> ({{ liveUpdatesPrice() }} once)</template> or a Paper pack in <a href="/profile?billing=1">Settings › Hosting & billing</a>, or take an author subscription.</p>
|
||
<div class="hosting-payment" v-if="detectedType === 'repo' && billing?.enabled">
|
||
<template v-if="hostingNeedsPayment() && !billingPending">
|
||
<p class="hosting-limit" role="status" v-if="options.expirationMode !== 'never' || hostingQuotaBlocked">{{ options.update && !hostingQuotaBlocked && hostingQuota?.repository?.used < hostingQuota?.repository?.total ? 'Automatic updates need a paid option for this repository.' : 'Your free repositories are all in use.' }}</p>
|
||
<fieldset class="hosting-options" v-if="billing.checkoutEnabled !== false && hostingOptionPlans().length">
|
||
<legend class="hosting-options-title">{{ hostingOneTime()?.id === 'repo_never' ? 'Keep this link online' : 'Choose how to host this repository' }}</legend>
|
||
<label class="hosting-option" v-for="plan in hostingOptionPlans()" :key="plan.id" :class="{ 'is-selected': hostingChoice() === plan.id || (plan.interval && authorPlans().some(p => p.id === hostingChoice())) }">
|
||
<input type="radio" name="hosting-plan" :value="plan.id" :checked="hostingChoice() === plan.id || (plan.interval && authorPlans().some(p => p.id === hostingChoice()))" @change="hostingPlan = plan.interval ? hostingSubscriptionPlan().id : plan.id">
|
||
<span class="hosting-option-body"><strong>{{ hostingOptionName(plan) }}</strong><small>{{ hostingOptionBlurb(plan) }}</small>
|
||
<span class="hosting-cycle billing-toggle" v-if="plan.interval && authorPlans().length > 1 && authorPlans().some(p => p.id === hostingChoice())" role="radiogroup" aria-label="Billing frequency"><button type="button" role="radio" v-for="cycle in authorPlans()" :key="cycle.id" :aria-checked="hostingSubscriptionPlan().id === cycle.id ? 'true' : 'false'" :class="{ 'is-on': hostingSubscriptionPlan().id === cycle.id }" @click.prevent="hostingCycle = cycle.id; hostingPlan = cycle.id">{{ cycle.interval === 'year' ? 'Yearly' : 'Monthly' }}<span class="billing-toggle-save" v-if="cycle.interval === 'year' && hostingAnnualSavings()">−{{ hostingAnnualSavings() }}%</span></button></span></span>
|
||
<span class="hosting-option-price"><strong>{{ billingAmount(plan.interval ? hostingSubscriptionPlan() : plan) }}</strong><small>{{ plan.interval ? 'per ' + hostingSubscriptionPlan().interval : 'once' }}</small></span>
|
||
</label>
|
||
</fieldset>
|
||
<p v-else role="status">New purchases are temporarily unavailable. Your draft is kept.</p>
|
||
<p class="hosting-terms" v-if="authorPlans().some(p => p.id === hostingChoice())">Renews {{ hostingChoice() === 'author_annual' ? 'yearly' : 'monthly' }} until you cancel. Tax shown at checkout.</p>
|
||
<p class="hosting-terms" v-else>One-time payment, nothing renews. Tax shown at checkout.</p>
|
||
<details class="capacity-options" v-if="options.expirationMode !== 'never'"><summary>Keep using free hosting</summary><p>Remove a repository you no longer need, or enter the code of a paid conference above. Removing a repository affects its review link.</p><a href="/dashboard">Review my repositories</a><button type="button" class="task-text-button" @click="anonymizeRepo($event)" :disabled="sourceLoading || billingBusy">Recheck capacity and submit</button></details>
|
||
</template>
|
||
</div>
|
||
<h4 class="hosting-subhead" v-if="hostingNeedsPayment() && !billingPending">Expiration</h4>
|
||
<fieldset class="expiration-choices">
|
||
<legend class="sr-only">Expiration</legend>
|
||
<label class="expiration-choice"><input type="radio" name="expiration-choice" :checked="options.expirationMode !== 'never'" @change="useDatedHosting()" :disabled="billingPending"><span>Expire on a date</span></label>
|
||
<div class="dated-options" v-if="options.expirationMode !== 'never'">
|
||
<div class="hosting-date" v-if="options?.expirationMode !== 'never'">
|
||
<label class="paper-field-label" for="expirationDate">Expires</label>
|
||
<input class="form-control" type="date" name="expirationDate" id="expirationDate" required v-field="{ state: viewState, set: value => { options.expirationDate = value }, value: options?.expirationDate, form: "anonymize", options: {} }" :class="{'is-invalid': anonymize?.expirationDate?.invalid}" :min="(minExpirationDate)" :max="(maxExpirationDate)">
|
||
<p class="date-echo" aria-live="polite" v-if="fmt?.dateEcho(options?.expirationDate)">Expires {{ fmt?.dateEcho(options?.expirationDate) }}</p>
|
||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.min">Pick a date in the future.</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.max">Pick a date on or before {{ fmt?.date(maxExpirationDate) }}.</div>
|
||
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.required || anonymize?.expirationDate?.errors?.date">Enter a valid expiration date.</div>
|
||
</div>
|
||
<div class="hosting-property"><label for="expiration">Then</label><select id="expiration" class="form-control" v-model="options.expirationMode"><option value="remove">Remove content</option><option value="redirect">Redirect to GitHub</option></select></div>
|
||
</div>
|
||
<label class="expiration-choice" v-if="detectedType === 'repo'"><input type="radio" name="expiration-choice" :checked="options.expirationMode === 'never'" @change="options.expirationMode = 'never'" :disabled="billingPending"><span>Never expire<small v-if="(options.expirationMode !== 'never' || hasAuthorHosting() || repositoryGrants('repo_never').length) && !(hostingNeedsPayment() && hostingOptionPlans().length)">{{ neverExpireTag() }}</small></span></label>
|
||
</fieldset>
|
||
<p class="form-text text-muted" v-if="options.expirationMode === 'redirect'">Visitors will be redirected to the original GitHub source on this date, revealing its identity.</p>
|
||
<div v-if="detectedType === 'repo' && billing?.enabled" class="inline-hosting">
|
||
<p v-if="taskValidationError" role="alert" class="text-danger">{{ taskValidationError }}</p>
|
||
<p v-if="billingError" role="alert" class="text-danger">{{ billingError }} <button type="button" class="task-text-button" @click="loadBilling()" :disabled="billingBusy">Reload billing</button></p>
|
||
<div v-if="billingPending" role="status"><p><strong>Confirming your payment…</strong></p><p class="form-text text-muted">Your draft is saved. Please do not pay again.</p></div>
|
||
<p v-else-if="billingNotice" role="status" class="form-text text-muted">{{ billingNotice }}</p>
|
||
<p class="hosting-terms" v-else-if="options.expirationMode === 'never' && !billingPending && !hasAuthorHosting() && repositoryGrants('repo_never').length">No scheduled expiration. Paid once for this repository.</p>
|
||
<p class="hosting-terms" v-else-if="options.expirationMode === 'never' && !billingPending">No scheduled expiration while subscribed. A 90-day grace period applies if paid access ends.</p>
|
||
<details class="repository-pass" v-if="isUpdate && role === 'owner' && !hasAuthorHosting() && !hostingNeedsPayment() && repositoryAddonPlans().length"><summary>Pay once for just this repository</summary><div v-for="plan in repositoryAddonPlans()" :key="plan.id"><p><strong>{{ plan.name }}</strong> · {{ billingPrice(plan) }}. {{ plan.description }}</p><button type="button" class="btn btn-outline-ink" :disabled="billingBusy || hostingSaving || sourceLoading || billing.checkoutEnabled === false || billingPending" @click="checkoutRepository(plan.id)">Buy {{ plan.name.toLowerCase() }}</button></div></details>
|
||
</div>
|
||
</section>
|
||
|
||
|
||
<section class="paper-settings-section" v-show="isUpdate && detectedType === 'repo'">
|
||
<h2 class="paper-section-eyebrow">Co-authors</h2>
|
||
<p class="form-text text-muted" style="margin-bottom: 8px;">
|
||
Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors.
|
||
</p>
|
||
|
||
<div class="form-group" v-show="role === 'owner' || role === 'admin'">
|
||
<label class="paper-field-label" for="coauthorSearch">Add a GitHub user</label>
|
||
<div style="position: relative;">
|
||
<input type="text" id="coauthorSearch" class="form-control" placeholder="Search GitHub username…" autocomplete="off" v-field="{ state: viewState, set: value => { coauthorSearch = value }, value: coauthorSearch, form: "anonymize", options: { debounce: 300 }, change: () => { searchCoauthors() } }">
|
||
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" v-show="coauthorResults?.length > 0">
|
||
<a href="#" class="dropdown-item d-flex align-items-center" v-for="(u, index) in coauthorResults" @click.prevent="addCoauthor(u, $event)">
|
||
<img alt="" style="width: 22px; height: 22px; border-radius: 50%; margin-right: 8px;" :src="safeUrl((u?.photo))">
|
||
<span v-text="u?.username"></span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<small class="form-text text-muted" v-show="coauthorError" v-text="coauthorError"></small>
|
||
</div>
|
||
|
||
<div class="coauthor-list">
|
||
<div class="coauthor-row d-flex align-items-center" style="padding: 6px 0; gap: 8px;" v-for="(c, index) in coauthors">
|
||
<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 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">
|
||
No co-authors yet.
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
|
||
|
||
</div>
|
||
<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">
|
||
<template v-if="detectedType === 'repo'">
|
||
<button type="button" class="btn btn-ink" v-if="billingPending" @click="refreshBilling()" :disabled="billingBusy">Check payment status</button>
|
||
<button type="submit" class="btn btn-ink" v-else :disabled="sourceLoading || billingBusy || hostingSaving || billingLoading || (hostingNeedsPayment() && ((!authorPlans().length && !hostingOneTime()) || billing?.checkoutEnabled === false))" @click.prevent="submitForm($event, () => { submitRepository($event) })">{{ hostingSaving ? 'Saving draft…' : hostingNeedsPayment() ? 'Continue to checkout' : isUpdate ? 'Save changes' : 'Create anonymous link' }}</button>
|
||
|
||
</template>
|
||
<button type="submit" :disabled="sourceLoading || !details" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||
Create anonymous link
|
||
</button>
|
||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
||
Save changes
|
||
</button>
|
||
<button type="submit" :disabled="sourceLoading || !details" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||
Create anonymous link
|
||
</button>
|
||
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
||
Save changes
|
||
</button>
|
||
<p class="submit-reason" role="status" v-if="sourceLoading">{{ detectedType === 'pr' ? 'Waiting for the pull request to load from GitHub…' : detectedType === 'gist' ? 'Waiting for the gist to load from GitHub…' : 'Waiting for the repository to load from GitHub…' }}</p>
|
||
<p class="submit-reason" role="status" v-else-if="!details && !isUpdate">Load an accessible source to continue. See the preview for details.</p>
|
||
<p class="checkout-return-note" v-if="hostingNeedsPayment() && !billingPending">You pay in Stripe, then come back here to finish.</p>
|
||
<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 ·" : "Unsaved changes ·" }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved" : "Discard" }}</button></div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Preview column (right): "What reviewers will see", a miniature reviewer window -->
|
||
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col reviewer-preview" v-if="detectedType">
|
||
<div class="rp-caption">
|
||
<span class="rp-title">What reviewers will see</span>
|
||
<span class="rp-count" v-if="previewStatus === 'ready' && previewRedactions() > 0" :title="previewRedactions() + ' redacted terms in this preview'"><span class="redact-slot" aria-hidden="true"><i>{{ previewRedactions() }}</i></span> redacted</span>
|
||
<span class="rp-count rp-count-none" v-else-if="previewStatus === 'ready' && !String(terms || '').trim()">No terms yet</span>
|
||
<span class="rp-count rp-count-none" v-else-if="previewStatus === 'ready' && readmePreserved()">README left unredacted</span>
|
||
<span class="rp-spacer"></span>
|
||
<a class="rp-open is-external" v-if="isUpdate && previewUrl()" :href="safeUrl(previewPath())" target="_blank" rel="noopener">Open reviewer page <span aria-hidden="true">↗</span></a>
|
||
<button type="button" class="rp-open is-toggle" v-else @click="previewExpanded = !previewExpanded" :aria-pressed="!!previewExpanded"><svg class="rp-open-icon" viewBox="0 0 16 16" aria-hidden="true"><path v-if="!previewExpanded" d="M9.5 2.5h4v4M13.5 2.5 9 7M6.5 13.5h-4v-4M2.5 13.5 7 9"/><path v-else d="M13.5 6.5h-4v-4M9.5 6.5 14 2M2.5 9.5h4v4M6.5 9.5 2 14"/></svg>{{ previewExpanded ? 'Back to settings' : 'Expand preview' }}</button>
|
||
</div>
|
||
<div class="rp-window">
|
||
<div class="rp-chrome" aria-hidden="true"><i></i><i></i><i></i><span class="rp-url">{{ previewUrl() || 'anonymous.4open.science' }}</span></div>
|
||
<div class="rp-bar" aria-hidden="true"><span class="vb-logo">Anonymous <em>GitHub</em></span><span class="rp-bar-id" v-if="previewId()">{{ previewId() }}</span><span class="rp-bar-kind" v-if="detectedType !== 'repo'">{{ detectedType === 'pr' ? 'Pull request' : 'Gist' }}</span></div>
|
||
<div class="rp-body" :class="{ 'has-side': detectedType === 'repo' }">
|
||
<!-- Sidebar: a miniature of the explorer's file tree -->
|
||
<nav class="rp-side" v-if="detectedType === 'repo'" aria-label="Files reviewers will see">
|
||
<ul class="files rp-files" v-if="previewTree().length">
|
||
<li v-for="entry in previewTree()" :key="entry.path" class="file" :class="{ folder: entry.dir, open: entry.open, active: entry.readme }" :style="{ '--depth': entry.depth }">
|
||
<a v-if="entry.dir" role="button" tabindex="0" :aria-expanded="entry.open ? 'true' : 'false'" :title="entry.path" @click="togglePreviewFolder(entry.path)" @keydown.enter.prevent="togglePreviewFolder(entry.path)" @keydown.space.prevent="togglePreviewFolder(entry.path)"><span class="tree-toggle"></span><span class="tree-icon-folder" aria-hidden="true"><svg viewBox="0 0 14 12" focusable="false"><path d="M1.5 10.5v-9h4l1.5 2h5.5v7z"/></svg></span><span class="tree-name">{{ entry.name }}</span></a>
|
||
<a v-else :title="entry.path" :aria-current="entry.readme ? 'page' : null"><span class="tree-spacer"></span><span class="far fa-file tree-icon-file" aria-hidden="true"></span><span class="tree-name">{{ entry.name }}</span></a>
|
||
</li>
|
||
</ul>
|
||
<ul class="rp-files-skel" v-else-if="preserve.loading || (sourceLoading && !preserve.error)" aria-label="Loading the file list"><li v-for="w in [58, 72, 46, 64, 52, 38, 68, 44]" :key="w"><i class="rp-skel" :style="{ width: w + '%' }"></i></li></ul>
|
||
<template v-else>
|
||
<ul class="files rp-files" v-if="readme || previewStatus === 'ready'"><li class="file active"><a title="README.md" aria-current="page"><span class="tree-spacer"></span><span class="far fa-file tree-icon-file" aria-hidden="true"></span><span class="tree-name">README.md</span></a></li></ul>
|
||
<p class="rp-side-note">Every other file at this commit appears here, with the same redactions.</p>
|
||
</template>
|
||
</nav>
|
||
<div class="rp-doc">
|
||
<div class="rp-file-head" v-if="detectedType === 'repo' && (previewStatus === 'ready' || readme)"><span class="rp-files-btn" aria-hidden="true"><svg class="ui-icon"><use href="#ui-icon-folder"></use></svg>Files<span class="rp-files-count" v-if="previewTreeCount()">{{ previewTreeCount() }}</span></span><span class="rp-file-name">README.md</span><span class="rp-file-meta">Markdown</span></div>
|
||
<!-- Loading: skeleton inside the window -->
|
||
<div class="rp-state rp-loading" role="status" v-if="previewStatus === 'loading' || (sourceLoading && previewStatus !== 'ready' && previewStatus !== 'error')">
|
||
<span class="rp-state-text">{{ previewStatus === 'loading' ? 'Updating the anonymized preview…' : (detectedType === 'pr' ? 'Loading the pull request from GitHub…' : detectedType === 'gist' ? 'Loading the gist from GitHub…' : 'Loading the repository from GitHub…') }}</span>
|
||
<i class="rp-skel rp-skel-title"></i><i class="rp-skel" style="width:92%"></i><i class="rp-skel" style="width:86%"></i><i class="rp-skel" style="width:58%"></i>
|
||
<i class="rp-skel rp-skel-block"></i><i class="rp-skel" style="width:80%"></i><i class="rp-skel" style="width:66%"></i>
|
||
</div>
|
||
<!-- The source itself could not be loaded -->
|
||
<div class="rp-state rp-error" role="alert" v-else-if="!details && detectedType !== 'repo'">
|
||
<strong>Couldn't load {{ sourceSummary() || (detectedType === 'pr' ? 'this pull request' : 'this gist') }}</strong>
|
||
<p>{{ error || 'GitHub did not return it. Check that the link is right and that Anonymous GitHub can read it.' }}</p>
|
||
<div class="rp-state-actions"><button type="button" class="btn btn-ink btn-sm" @click="urlSelected(true)">Retry</button><button type="button" class="btn btn-outline-ink btn-sm" v-if="detectedType === 'pr' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button><a class="btn btn-outline-ink btn-sm" v-else href="/connections">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a></div>
|
||
</div>
|
||
<div class="rp-state rp-error" role="alert" v-else-if="!details && detectedType === 'repo' && !isUpdate && error">
|
||
<strong>Couldn't load this repository</strong>
|
||
<p>{{ error }}</p>
|
||
<div class="rp-state-actions"><button type="button" class="btn btn-ink btn-sm" @click="urlSelected(true)">Retry</button><button type="button" class="btn btn-outline-ink btn-sm" v-if="githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button></div>
|
||
</div>
|
||
<!-- The anonymized preview failed -->
|
||
<div class="rp-state rp-error" role="alert" v-else-if="previewStatus === 'error'">
|
||
<strong>Couldn't apply the redactions</strong>
|
||
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
||
<div class="rp-state-actions"><button class="btn btn-ink btn-sm" type="button" @click="retryPreview()">Retry preview</button></div>
|
||
</div>
|
||
<div class="rp-state rp-empty" v-else-if="detectedType === 'repo' && !readme && previewStatus === 'idle'">
|
||
<strong>No README</strong>
|
||
<p>Reviewers land on the file list. Open the files you will share and check them before sending the link.</p>
|
||
</div>
|
||
<div class="anonymize-preview-body markdown-body body" v-else-if="detectedType === 'repo' && previewStatus === 'ready'" v-html="renderRedacted(sanitize(html_readme))"></div>
|
||
<div class="anonymize-preview-body" v-else-if="detectedType === 'gist' && 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>
|
||
<span class="badge" :class="{'badge-success': details?.gist?.isPublic, 'badge-secondary': !details?.gist?.isPublic}">
|
||
{{ details?.gist?.isPublic ? 'public' : 'secret' }}
|
||
</span>
|
||
</h2>
|
||
<small v-if="options?.date" v-text="fmt?.date(details?.gist?.updatedDate)"></small>
|
||
</div>
|
||
<small v-if="options?.origin">Gist ID: {{ details?.source?.gistId }}</small>
|
||
<small v-if="options?.username && details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
|
||
<div v-if="options?.content && previewGistFiles?.length">
|
||
<ul class="pr-comments mt-3">
|
||
<li class="pr-comment" v-for="(file, index) in previewGistFiles">
|
||
<div class="pr-comment-head">
|
||
<strong v-text="file?.filename"></strong>
|
||
<span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
|
||
</div>
|
||
<gist-file :file="file" :terms="terms" :options="options"></gist-file>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div v-if="options?.comments && details?.gist?.comments && details?.gist?.comments?.length">
|
||
<h3 class="paper-section-eyebrow mt-3">Comments</h3>
|
||
<ul class="pr-comments">
|
||
<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">
|
||
<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>
|
||
<div class="pr-comment-body" v-if="options?.body">
|
||
<markdown :content="anonymizeGistContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="anonymize-preview-body" v-else-if="detectedType === 'pr' && 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>
|
||
<span class="badge" :class="{'badge-success':details?.pullRequest?.merged, 'badge-warning':details?.pullRequest?.state=='open', 'badge-danger':details?.pullRequest?.state=='closed' && !details?.pullRequest?.merged}">
|
||
{{ fmt?.title(details?.pullRequest?.merged ? "merged" : details?.pullRequest?.state) }}
|
||
</span>
|
||
</h2>
|
||
<small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
|
||
</div>
|
||
<small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
|
||
<div class="pr-body" v-if="options?.body">
|
||
<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'}" 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'}" 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 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 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">
|
||
<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>
|
||
<div class="pr-comment-body" v-if="options?.body">
|
||
<markdown :content="anonymizePrContent(comment?.body)" :options="options" :terms="terms"></markdown>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- More below: a fade with a way to keep reading (hidden by CSS once the window is scrolled to the end) -->
|
||
<div class="rp-more" v-if="previewStatus === 'ready'">
|
||
<a class="rp-more-btn" v-if="isUpdate && previewUrl()" :href="safeUrl(previewPath())" target="_blank" rel="noopener">Continue reading on the reviewer page <span aria-hidden="true">↗</span></a>
|
||
<button type="button" class="rp-more-btn" v-else-if="!previewExpanded" @click="previewExpanded = true">Continue reading in the expanded preview</button>
|
||
<span class="rp-more-btn is-hint" v-else aria-hidden="true">Scroll for more</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|