mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
537 lines
49 KiB
HTML
537 lines
49 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">
|
|
<div class="paper-crumbs">My work / <span class="here">New anonymization</span></div>
|
|
<h1 class="paper-page-title">New <em>anonymization</em></h1>
|
|
<p class="paper-page-lede">
|
|
Paste a GitHub repository, pull-request, or gist URL. We’ll fetch
|
|
it, redact the terms you choose in text files, and give you a share link.
|
|
Images and PDFs are served unchanged when enabled. Review them before sharing.
|
|
</p>
|
|
<div class="form-group mt-4 mb-2">
|
|
<div class="source-url-label-row">
|
|
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
|
|
<aside class="repo-access" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
|
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate && detectedType !== 'gist'">
|
|
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><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 => { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }, change: () => { urlSelected() } }">
|
|
</div>
|
|
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
|
<p class="text-danger" role="alert" v-if="sourceUrlError">{{ sourceUrlError }}</p>
|
|
<small class="form-text" style="color: var(--ink-muted);">
|
|
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
|
|
</small>
|
|
<aside class="repo-access-details" aria-label="GitHub access" v-if="!sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
|
<div class="repo-access-main">
|
|
<div class="repo-access-controls">
|
|
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
|
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
|
</div>
|
|
</div>
|
|
<div class="repo-access-note"><span><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>
|
|
</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="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' ? 'Repo' : detectedType === 'pr' ? 'PR' : 'Gist' }}
|
|
</span>
|
|
</div>
|
|
</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')">Settings</button>
|
|
<button id="workspace-preview-tab" role="tab" type="button" :aria-selected="mobilePane === 'preview'" aria-controls="workspace-preview" :tabindex="mobilePane === 'preview' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('preview')">Reviewer preview</button>
|
|
</nav>
|
|
<div class="anonymize-preview-empty" id="workspace-preview" v-if="detectedType !== 'repo' && !details" role="status">{{ sourceLoading ? 'Loading source…' : 'Choose an accessible source to see its preview.' }}</div>
|
|
<div class="anonymize-split">
|
|
<!-- Form column (left) -->
|
|
<div 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">GitHub URL</label>
|
|
<aside class="repo-access" aria-label="GitHub access" v-if="sourceUrl && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
|
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate && detectedType !== 'gist'">
|
|
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><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 && 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}">
|
|
<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 && githubConnections?.appEnabled && githubConnections.appConnected && githubConnections.oauthConnected">
|
|
<div class="repo-access-main">
|
|
<div class="repo-access-controls">
|
|
<button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' && githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
|
|
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">{{ isUpdate ? 'Change connection' : 'Manage connections' }} →</a>
|
|
</div>
|
|
</div>
|
|
<div class="repo-access-note"><span><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>
|
|
|
|
<aside class="repo-access-details" v-if="isUpdate && detectedType === 'repo' && role === 'owner' && 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 === '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" v-show="detectedType">
|
|
<input class="form-check-input" type="checkbox" id="update" name="update" 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</label>
|
|
<small class="form-text text-muted">Follows the latest version, at most once an hour.</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">anonymous.4open.science/r/{{ repoId }}</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">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 === '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">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="terms">Terms to redact</label>
|
|
<textarea class="form-control" id="terms" name="terms" rows="3" 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="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">
|
|
<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>
|
|
<p class="hosting-usage" v-if="detectedType === 'repo' && billing?.enabled">
|
|
<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="conference">Conference hosting is checked on submission.</span>
|
|
<span v-else-if="hostingQuota?.repository">{{ hostingQuota.repository.used }} of {{ hostingQuota.repository.total }} active repositories · Free</span>
|
|
</p>
|
|
<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)">
|
|
<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()">{{ hasAuthorHosting() ? 'Included in your subscription' : 'Author subscription' }}</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 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}">
|
|
<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>
|
|
<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>
|
|
<template v-if="hostingNeedsPayment() && !billingPending">
|
|
<p v-if="options.expirationMode !== 'never'" class="capacity-reason">{{ options.update && !hostingQuotaBlocked && hostingQuota?.repository?.used < hostingQuota?.repository?.total ? 'Automatic updates require Author hosting.' : 'Your free hosting allowance is full.' }}</p>
|
|
<div class="subscription-choice" v-if="authorPlans().length && billing.checkoutEnabled !== false">
|
|
<h3>Author subscription</h3>
|
|
<fieldset class="billing-cycle"><legend class="sr-only">Billing frequency</legend><label class="cycle-option" v-for="plan in authorPlans()" :key="plan.id" :class="{'selected': selectedHostingPlan()?.id === plan.id}"><input type="radio" name="hosting-plan" :value="plan.id" :checked="selectedHostingPlan()?.id === plan.id" @change="hostingPlan = plan.id"><span>{{ billingPrice(plan) }}</span></label></fieldset>
|
|
<p>{{ selectedHostingPlan()?.repositories }} repositories, {{ billingBytes(selectedHostingPlan()?.storage) }}. Coauthors included.</p>
|
|
<p class="hosting-terms">Renews {{ selectedHostingPlan()?.interval === 'year' ? 'annually' : 'monthly' }} until cancelled. Tax shown at checkout.</p>
|
|
<p class="hosting-terms">If paid access ends, never-expiring repositories expire after 90 days.</p>
|
|
</div>
|
|
<p v-else role="status">New purchases are temporarily unavailable. Your draft is kept.</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 use conference hosting. 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>
|
|
<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()"><summary>Hosting just this repository?</summary><div v-for="plan in (billing?.plans || []).filter(p => p.id === 'author_pass')" :key="plan.id"><p>{{ billingPrice(plan) }} · {{ billingBytes(plan.storage) }} for 180 days. No renewal. Does not include Never expire.</p><button type="button" class="btn btn-outline-ink" :disabled="billingBusy || hostingSaving || sourceLoading || billing.checkoutEnabled === false || billingPending" @click="checkoutRepository(true)">Review repository pass</button></div></details>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
<section class="paper-settings-section" v-show="isUpdate && detectedType === 'repo'">
|
|
<h2 class="paper-section-eyebrow">Coauthors</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 class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
|
|
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved in this tab for 30 minutes." : "Unsaved changes. Keep this tab open until you save." }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved settings" : "Discard changes" }}</button></div>
|
|
|
|
</div>
|
|
<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 || billing?.checkoutEnabled === false))" @click.prevent="submitForm($event, () => { submitRepository($event) })">{{ hostingSaving ? 'Saving draft…' : hostingNeedsPayment() ? 'Continue to checkout' : isUpdate ? 'Save changes' : 'Create anonymous link' }}</button>
|
|
<p class="checkout-return-note" v-if="hostingNeedsPayment() && !billingPending">Review payment in Stripe, then return to finish.</p>
|
|
</template>
|
|
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
|
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Pull Request
|
|
</button>
|
|
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'pr' && isUpdate" @click.prevent="submitForm($event, () => { anonymizePullRequest($event) })">
|
|
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Pull Request
|
|
</button>
|
|
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && !isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
|
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize Gist
|
|
</button>
|
|
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === 'gist' && isUpdate" @click.prevent="submitForm($event, () => { anonymizeGist($event) })">
|
|
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update Gist
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Preview column (right) -->
|
|
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'repo'">
|
|
<div class="anonymize-preview-head">
|
|
<strong>README.md</strong><button type="button" class="task-text-button preview-expand" @click="previewExpanded = !previewExpanded">{{ previewExpanded ? 'Restore split view' : 'Expand preview' }}</button>
|
|
</div>
|
|
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
|
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
|
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
|
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
|
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
|
</div>
|
|
<div class="anonymize-preview-status" v-if="!sourceLoading && !readme && previewStatus === 'idle'">No README is available to preview. Review the published files before sharing.</div>
|
|
<div class="anonymize-preview-body markdown-body body" v-if="previewStatus === 'ready'" v-html="sanitize(html_readme)"></div>
|
|
</div>
|
|
|
|
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'gist' && details">
|
|
<div class="anonymize-preview-head">
|
|
<strong>Gist preview</strong><button type="button" class="task-text-button preview-expand" @click="previewExpanded = !previewExpanded">{{ previewExpanded ? 'Restore split view' : 'Expand preview' }}</button>
|
|
</div>
|
|
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
|
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
|
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
|
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
|
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
|
</div>
|
|
<div class="anonymize-preview-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="{'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>
|
|
|
|
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col" v-if="detectedType === 'pr' && details">
|
|
<div class="anonymize-preview-head">
|
|
<strong>Pull request preview</strong><button type="button" class="task-text-button preview-expand" @click="previewExpanded = !previewExpanded">{{ previewExpanded ? 'Restore split view' : 'Expand preview' }}</button>
|
|
</div>
|
|
<div class="anonymize-preview-status" role="status" v-if="sourceLoading && previewStatus === 'idle'">Loading source…</div>
|
|
<div class="anonymize-preview-status" role="status" v-if="previewStatus === 'loading'">Updating the anonymized preview…</div>
|
|
<div class="anonymize-preview-status" role="alert" v-if="previewStatus === 'error'">
|
|
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
|
|
<button class="btn btn-outline-ink" type="button" @click="retryPreview()">Retry preview</button>
|
|
</div>
|
|
<div class="anonymize-preview-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="{'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 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="{'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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|