Files
anonymous_github/public/partials/anonymize.htm
T

528 lines
47 KiB
HTML

<div class="anonymize-page h-100" :class="{'mobile-preview-active': mobilePane === 'preview'}">
<!-- ===== STATE 1: No URL — centered input ===== -->
<div class="anonymize-landing" v-show="!workspaceOpen &amp;&amp; !isUpdate">
<div class="anonymize-landing-inner">
<div class="paper-crumbs">My work &nbsp;/&nbsp; <span class="here">New anonymization</span></div>
<h1 class="paper-page-title">New <em>anonymization</em></h1>
<p class="paper-page-lede">
Paste a GitHub repository, pull-request, or gist URL. We&rsquo;ll fetch
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 &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected &amp;&amp; githubConnections.oauthConnected">
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate &amp;&amp; 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 id="sourceUrl-landing" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value =&gt; { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { urlSelected() } }">
</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>
<aside class="repo-access-details" aria-label="GitHub access" v-if="!sourceUrl &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected">
<div class="repo-access-main">
<div class="repo-access-controls">
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' &amp;&amp; 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' }} &rarr;</a>
</div>
</div>
<div class="repo-access-note"><span v-if="detectedType !== 'gist' &amp;&amp; githubConnection !== 'oauth'">Signing in does not grant access to private repositories. Use Manage access to install or configure the GitHub App, 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> 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="workspaceOpen || isUpdate">
<header class="anonymize-topbar">
<div class="anonymize-topbar-inner">
<div class="paper-crumbs">
<a href="/dashboard">My work</a> &nbsp;/&nbsp;
<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 <em>anonymization</em></span>
<span v-if="isUpdate">Edit <em>anonymization</em></span>
</h1>
<span class="type-badge" v-show="detectedType" :class="{&#x27;type-repo&#x27;: detectedType === &#x27;repo&#x27;, &#x27;type-pr&#x27;: detectedType === &#x27;pr&#x27;, &#x27;type-gist&#x27;: detectedType === &#x27;gist&#x27;}">
{{ detectedType === 'repo' ? 'Repo' : detectedType === 'pr' ? 'PR' : 'Gist' }}
</span>
</div>
</div>
</header>
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' &amp;&amp; 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' &amp;&amp; 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')">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' &amp;&amp; !details" role="status">{{ sourceLoading ? 'Loading source…' : 'Choose an accessible source to see its preview.' }}</div>
<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">
<section class="paper-settings-section">
<div class="paper-section-eyebrow">Source</div>
<div class="form-group">
<div class="source-url-label-row">
<label class="paper-field-label" for="sourceUrl">GitHub URL</label>
<aside class="repo-access" aria-label="GitHub access" v-if="sourceUrl &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected &amp;&amp; githubConnections.oauthConnected">
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate &amp;&amp; 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" :disabled="isUpdate &amp;&amp; detectedType !== &#x27;repo&#x27;" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value =&gt; { sourceUrl = value }, value: sourceUrl, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { urlSelected() } }" :class="{&#x27;is-invalid&#x27;: anonymize?.sourceUrl?.invalid}">
<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="GitHub access" v-if="sourceUrl &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected">
<div class="repo-access-main">
<div class="repo-access-controls">
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' &amp;&amp; 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' }} &rarr;</a>
</div>
</div>
<div class="repo-access-note"><span v-if="detectedType !== 'gist' &amp;&amp; githubConnection !== 'oauth'">Signing in does not grant access to private repositories. Use Manage access to install or configure the GitHub App, 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> Selected content will be published, including content from private repositories.</span></div>
</aside>
<aside class="repo-access-details" v-if="isUpdate &amp;&amp; detectedType === 'repo' &amp;&amp; role === 'owner' &amp;&amp; accessRepairAllowed &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnection === 'github-app' &amp;&amp; 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 access</button>
<p v-if="accessRepairMessage" role="status">{{ accessRepairMessage }}</p>
</aside>
<aside class="repo-access-details" v-if="isUpdate &amp;&amp; detectedType === 'repo' &amp;&amp; role === 'owner' &amp;&amp; sourceUrl === 'https://github.com/' + _originalFullName">
<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>
</aside>
<div class="form-grid-2" v-if="detectedType === &#x27;repo&#x27;">
<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 =&gt; { source.branch = value }, value: source?.branch, form: &quot;anonymize&quot;, 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" 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>
</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 =&gt; { source.commit = value }, value: source?.commit, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: 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" v-show="detectedType">
<input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value =&gt; { options.update = value }, value: options?.update, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="update">Auto-update from GitHub</label>
<small class="form-text text-muted">Follow the branch and pull the latest commit automatically, at most once an hour.</small>
</div>
</section>
<section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Identity</div>
<div class="form-group" v-show="detectedType === &#x27;repo&#x27;">
<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 =&gt; { repoId = value }, value: repoId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.repoId?.invalid}">
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/r/{{ repoId }}</code>.</small>
<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 === &#x27;pr&#x27;">
<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 =&gt; { pullRequestId = value }, value: pullRequestId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.pullRequestId?.invalid}">
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/pr/{{ pullRequestId }}</code>.</small>
<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 === &#x27;gist&#x27;">
<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 =&gt; { gistId = value }, value: gistId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.gistId?.invalid}">
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/gist/{{ gistId }}</code>.</small>
<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="conference">Conference <span class="paper-optional">(optional)</span></label>
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value =&gt; { conference = value }, value: conference, form: &quot;anonymize&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.conference?.invalid}">
<small class="form-text text-muted" v-show="conference_data">
<a target="_blank" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a> expires {{ fmt?.date(conference_data?.endDate) }}.
</small>
<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">Link to a conference to apply its shared defaults.</small>
</div>
</section>
<section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Anonymization</div>
<div class="form-group">
<label class="paper-field-label" for="terms">Terms to redact</label>
<textarea class="form-control" id="terms" name="terms" rows="4" v-field="{ state: viewState, set: value =&gt; { terms = value }, value: terms, form: &quot;anonymize&quot;, options: { debounce: 250 } }" :class="{&#x27;is-invalid&#x27;: anonymize?.terms?.invalid}"></textarea>
<small class="form-text text-muted">One term per line (regex allowed). Replaced by <code>{{ site_options?.ANONYMIZATION_MASK }}-[N]</code>, or use <code>term=&gt;replacement</code> to pick your own (e.g. <code>Anonymous=&gt;ABC</code>).</small>
<div class="warning-feedback" v-show="termsRegexWarning">Regex characters detected. Escape them if unintentional.</div>
<div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">Terms are in an invalid format.</div>
</div>
</section>
<section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Display</div>
<div v-show="detectedType === &#x27;repo&#x27;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value =&gt; { options.link = value }, value: options?.link, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="link">Keep links</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value =&gt; { options.image = value }, value: options?.image, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="image">Display images</label>
<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 =&gt; { options.pdf = value }, value: options?.pdf, form: &quot;anonymize&quot;, 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 =&gt; { options.notebook = value }, value: options?.notebook, form: &quot;anonymize&quot;, 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 =&gt; { options.page = value }, value: options?.page, form: &quot;anonymize&quot;, options: {} }" :disabled="!details?.hasPage || (details?.pageSource &amp;&amp; details?.pageSource?.branch !== source?.branch)">
<label class="form-check-label" for="page">GitHub Pages</label>
<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 &amp;&amp; details?.pageSource?.branch &amp;&amp; source?.branch &amp;&amp; details.pageSource.branch !== source.branch">
{{ fmt?.translate('WARNINGS.page_branch_mismatch', { pageBranch: details?.pageSource?.branch, selectedBranch: source?.branch }) }}
</small>
</div>
</div>
<div v-show="detectedType === &#x27;gist&#x27;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" v-field="{ state: viewState, set: value =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="title-gist">Gist description</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" v-field="{ state: viewState, set: value =&gt; { options.content = value }, value: options?.content, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="content-gist">File contents</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" v-field="{ state: viewState, set: value =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="comments-gist">Comments</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" v-field="{ state: viewState, set: value =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="username-gist">Usernames</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" v-field="{ state: viewState, set: value =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="date-gist">Dates</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" v-field="{ state: viewState, set: value =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="origin-gist">Source gist ID</label>
</div>
</div>
<div v-show="detectedType === &#x27;pr&#x27;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="title" name="title" v-field="{ state: viewState, set: value =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="title">PR title</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="body" name="body" v-field="{ state: viewState, set: value =&gt; { options.body = value }, value: options?.body, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="body">PR body</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="diff" name="diff" v-field="{ state: viewState, set: value =&gt; { options.diff = value }, value: options?.diff, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="diff">Diff</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="comments" name="comments" v-field="{ state: viewState, set: value =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="comments">Comments</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="username" name="username" v-field="{ state: viewState, set: value =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="username">Usernames</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="date" name="date" v-field="{ state: viewState, set: value =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="date">Dates</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="origin" name="origin" v-field="{ state: viewState, set: value =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="origin">Project name</label>
</div>
</div>
</section>
<section class="paper-settings-section" v-show="detectedType">
<div class="paper-section-eyebrow">Expiration</div>
<div class="form-grid-2">
<div class="form-group">
<label class="paper-field-label" for="expiration">Strategy</label>
<select class="form-control" id="expiration" name="expiration" v-field="{ state: viewState, set: value =&gt; { options.expirationMode = value }, value: options?.expirationMode, form: &quot;anonymize&quot;, options: {} }">
<option value="redirect">Redirect to GitHub when expired</option>
<option value="remove" selected>Remove when expired</option>
</select>
</div>
<div class="form-group">
<label class="paper-field-label" for="expirationDate">Expiration date</label>
<input class="form-control" type="date" name="expirationDate" id="expirationDate" required v-field="{ state: viewState, set: value =&gt; { options.expirationDate = value }, value: options?.expirationDate, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: anonymize?.expirationDate?.invalid}" :min="(minExpirationDate)" :max="(maxExpirationDate)">
<div class="invalid-feedback" 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>
<small class="form-text text-muted" v-show="options?.expirationMode==&#x27;remove&#x27; &amp;&amp; options?.expirationDate">After {{ fmt?.date(options?.expirationDate) }}, the content will be removed.</small>
<small class="form-text text-muted" v-show="options?.expirationMode==&#x27;redirect&#x27; &amp;&amp; options?.expirationDate">After {{ fmt?.date(options?.expirationDate) }}, visitors will be redirected to GitHub.</small>
</section>
<section class="paper-settings-section" v-show="isUpdate &amp;&amp; detectedType === &#x27;repo&#x27;">
<div class="paper-section-eyebrow">Co-authors</div>
<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 === &#x27;owner&#x27; || role === &#x27;admin&#x27;">
<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 =&gt; { coauthorSearch = value }, value: coauthorSearch, form: &quot;anonymize&quot;, options: { debounce: 300 }, change: () =&gt; { searchCoauthors() } }">
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" v-show="coauthorResults?.length &gt; 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(&quot;https://github.com/&quot; + (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 === &#x27;owner&#x27; || role === &#x27;admin&#x27;" @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 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' &amp;&amp; (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" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;repo&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeRepo($event) })">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Repository
</button>
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;repo&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeRepo($event) })">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Repository
</button>
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { 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" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { 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" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeGist($event) })">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Gist
</button>
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { 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 id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === &#x27;repo&#x27;">
<div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'README with redactions applied' : 'README preview' }}</span>
</div>
<div class="anonymize-preview-status" role="status" v-if="sourceLoading &amp;&amp; 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 &amp;&amp; !readme &amp;&amp; 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 id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; details">
<div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'Gist with redactions applied' : 'Gist preview' }}</span>
</div>
<div class="anonymize-preview-status" role="status" v-if="sourceLoading &amp;&amp; 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>
<span class="badge" :class="{&#x27;badge-success&#x27;: details?.gist?.isPublic, &#x27;badge-secondary&#x27;: !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 &amp;&amp; details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
<div v-if="options?.content &amp;&amp; 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 &amp;&amp; details?.gist?.comments &amp;&amp; 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>
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; details">
<div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">{{ previewStatus === 'ready' ? 'Pull request with redactions applied' : 'Pull request preview' }}</span>
</div>
<div class="anonymize-preview-status" role="status" v-if="sourceLoading &amp;&amp; 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>
<span class="badge" :class="{&#x27;badge-success&#x27;:details?.pullRequest?.merged, &#x27;badge-warning&#x27;:details?.pullRequest?.state==&#x27;open&#x27;, &#x27;badge-danger&#x27;:details?.pullRequest?.state==&#x27;closed&#x27; &amp;&amp; !details?.pullRequest?.merged}">
{{ fmt?.title(details?.pullRequest?.merged ? "merged" : details?.pullRequest?.state) }}
</span>
</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 shadow-sm p-3 mb-4 rounded" style="background: var(--paper-bg-alt)" v-if="options?.body">
<markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
</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="{&#x27;active&#x27;: prTabState?.active == &#x27;diff&#x27;}" 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 = &#x27;diff&#x27;">
<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="{&#x27;active&#x27;: prTabState?.active == &#x27;comments&#x27;}" 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 = &#x27;comments&#x27;">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></span>
</button>
</nav>
<div class="anonymize-preview-status" v-if="!options?.diff &amp;&amp; !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 &amp;&amp; prTabState?.active == &#x27;diff&#x27;">
<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 &amp;&amp; prTabState?.active == &#x27;comments&#x27;">
<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>
</div>
</div>
</div>
</div>