Files
Thomas DurieuxandClaude Opus 5.5 c044b83541 feat(ui): one consistent design across author, reviewer and admin screens
The UI was uneven: the home page looked finished, but app pages used three
different display typefaces, the anonymize flow changed frame between steps,
and the dashboard, explorer and admin screens each had their own controls,
status colours and edge-case handling.

Make every screen follow one system: shared page header, grid, buttons,
filters and status colours; a repository picker and step trail on anonymize;
a "What reviewers will see" preview of the actual reviewer page; consistent
file headers and viewers for every file type; truthful admin charts and
tables. Fold the accumulated override blocks back into style.css (22%
smaller, no visual change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 02:22:45 +00:00

236 lines
17 KiB
HTML

<div class="container-fluid h-100 anonymize-page">
<div class="row h-100 flex-column flex-md-row">
<div class="col-md sidePanel shadow overflow-auto anonymize-form-col">
<div class="p-0 py-2 m-auto" :class="{&#x27;card&#x27;: !pullRequestUrl,&#x27;container&#x27;: pullRequestUrl}">
<form class="form needs-validation" name="anonymizeForm" novalidate v-form="viewState" :class="{&#x27;card-body&#x27;: !pullRequestUrl}">
<div class="paper-crumbs">Anonymize &nbsp;/&nbsp; <span class="here">Pull request</span></div>
<h1 class="paper-page-title">Anonymize a <em>pull request</em></h1>
<p class="paper-page-lede">
Fill in the details &mdash; it only takes a minute.
</p>
<div class="paper-section-eyebrow anonymize-section-title">
<i class="fab fa-github"></i> Source
</div>
<!-- pullRequestUrl -->
<div class="form-group">
<label for="pullRequestUrl">URL of your pull request</label>
<input type="text" class="form-control" name="pullRequestUrl" id="pullRequestUrl" placeholder="https://github.com/owner/repo/pull/123" v-field="{ state: viewState, set: value =&gt; { pullRequestUrl = value }, value: pullRequestUrl, form: &quot;anonymizeForm&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { pullRequestSelected() } }" :class="{&#x27;is-invalid&#x27;: anonymize?.pullRequestUrl?.invalid}">
<div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.access">
{{ pullRequestUrl }} is not accessible. Some organizations are
restricting the access to the repositories.
</div>
<div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.missing">
{{ pullRequestUrl }} does not exist or is not accessible
</div>
<div class="invalid-feedback" v-show="anonymize?.pullRequestUrl?.errors?.used">
{{ pullRequestUrl }} is already anonymized
</div>
</div>
<div v-show="pullRequestUrl">
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="update" name="update" v-field="{ state: viewState, set: value =&gt; { options.update = value }, value: options?.update, form: &quot;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="update">Auto update</label>
<small id="updateHelp" class="form-text text-muted">Automatically update the anonymized pull request with the
latest updates. The pull request is updated once per day
maximum.</small>
</div>
</div>
<div class="paper-section-eyebrow anonymize-section-title">
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conference ID
</div>
<!-- Conference -->
<div class="form-group">
<label for="conference">Conference ID <span class="text-muted">(Optional)</span></label>
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value =&gt; { conference = value }, value: conference, form: &quot;anonymizeForm&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.conference?.invalid}">
<small class="form-text text-muted" v-show="conference_data"><a target="_target" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a>
will expire on {{ 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">
Use the Conference ID that your conference provided you. This
will update automatically the anonymization options based on the
conference preferences.
</small>
</div>
<div class="paper-section-eyebrow anonymize-section-title">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymization Options
</div>
<!-- Pull Request ID -->
<div class="form-group">
<label for="pullRequestId">Anonymized pull request id</label>
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" v-field="{ state: viewState, set: value =&gt; { pullRequestId = value }, value: pullRequestId, form: &quot;anonymizeForm&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.pullRequestId?.invalid}">
<small id="idHelp" class="form-text text-muted">Id used in the url:
https://anonymous.4open.science/r/{{ pullRequestId }}</small>
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">
Repository id can only contain letters and numbers
</div>
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">
{{ pullRequestId }} is already used
</div>
</div>
<!-- Terms -->
<div class="form-group">
<label for="terms">Terms to anonymize</label>
<textarea class="form-control" id="terms" name="terms" rows="3" v-field="{ state: viewState, set: value =&gt; { terms = value }, value: terms, form: &quot;anonymizeForm&quot;, options: { debounce: 250 } }" :class="{&#x27;is-invalid&#x27;: anonymize?.terms?.invalid}"></textarea>
<small id="termsHelp" class="form-text text-muted">One term per line. Each term will be replaced by XXX.</small>
<div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">
Terms are in an invalid format
</div>
</div>
<div class="form-group">
<label for="expiration">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;anonymizeForm&quot;, options: {} }">
<option value="never" selected>Never expire</option>
<option value="redirect">
Redirect to GitHub when expired
</option>
<option value="remove">Remove when expired</option>
</select>
<small class="form-text text-muted">Define the expiration strategy for the anonymized
repository.</small>
</div>
<div class="form-group" id="expiration-date-form" v-show="!(options?.expirationMode==&#x27;never&#x27;)">
<label for="expirationDate">Expiration date of the anonymized repository</label>
<input class="form-control" type="date" name="expirationDate" id="expirationDate" v-field="{ state: viewState, set: value =&gt; { options.expirationDate = value }, value: options?.expirationDate, form: &quot;anonymizeForm&quot;, options: {} }">
<p class="date-echo" aria-live="polite" v-if="fmt?.dateEcho(options?.expirationDate)">Expires {{ fmt?.dateEcho(options?.expirationDate) }}</p>
<small class="form-text text-muted" v-show="options?.expirationMode==&#x27;remove&#x27;">After {{ fmt?.date(options?.expirationDate) }}, the repository will be
removed and the visitor will not be able to see the content of
the repository.</small>
<small class="form-text text-muted" v-show="options?.expirationMode==&#x27;redirect&#x27;">After {{ fmt?.date(options?.expirationDate) }}, the visitors of the
anonymized repository will be redirected to
{{ pullRequestUrl }}.</small>
</div>
<div class="accordion mb-3" id="options">
<div class="card">
<div class="card-header" id="headingOne">
<h2 class="mb-0">
<button class="btn btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#project-icon-filter"></use></svg> Advanced options
</button>
</h2>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#options">
<div class="card-body">
<div class="form-group mb-0">
<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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="link">Keep links</label>
<small class="form-text text-muted">Keep or remove all the links.</small>
</div>
<div 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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="image">Display images</label>
<small class="form-text text-muted">Images are not anonymized</small>
</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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="date">Display dates</label>
<small class="form-text text-muted">Display the date of the Pull Request and the date of
the comments.</small>
</div>
<div 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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="username">Display username</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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="comments">Display comments</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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="diff">Display diff</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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="origin">Display the project name</label>
</div>
<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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="title">Display the 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;anonymizeForm&quot;, options: {} }">
<label class="form-check-label" for="body">Display the PR body and comment bodies</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
<div class="anonymize-submit-bar" v-show="pullRequestUrl">
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="!isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizePullRequest($event) })">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> Anonymize
</button>
<button id="submit" type="submit" class="btn btn-ink btn-block" v-if="isUpdate" @click.prevent="submitForm($event, () =&gt; { updatePullRequest($event) })">
<svg class="ui-icon mr-1" aria-hidden="true"><use href="#ui-icon-save"></use></svg> Update
</button>
</div>
</form>
</div>
</div>
<div class="col-md-8 p-2 overflow-auto anonymize-preview-col" v-if="details">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1">
<span v-if="options?.title">{{ anonymize(details?.pullRequest?.title) }}</span>
<span class="badge" :class="{&#x27;badge-success&#x27;:details?.pullRequest?.merged, &#x27;badge-warning&#x27;:details?.pullRequest?.state==&#x27;open&#x27;, &#x27;badge-danger&#x27;:details?.pullRequest?.state==&#x27;closed&#x27; &amp;&amp;!details?.pullRequest?.merged}">
{{ fmt?.title(details?.pullRequest?.merged?"merged":details?.pullRequest?.state) }}
</span>
</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(--sidebar-bg-color)" v-if="options?.body">
<markdown :content="anonymize(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation" v-if="options?.diff">
<button class="nav-link active" id="pills-diff-tab" data-toggle="pill" data-target="#pills-diff" type="button" role="tab" aria-controls="pills-diff" aria-selected="true">
Diff
</button>
</li>
<li class="nav-item" role="presentation" v-if="options?.comments">
<button class="nav-link" id="pills-comments-tab" data-toggle="pill" data-target="#pills-comments" type="button" role="tab" aria-controls="pills-comments" aria-selected="false" :class="{&#x27;active&#x27;:!options?.diff}">
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {&#x27;0&#x27;: &#x27;No comment&#x27;,
&#x27;one&#x27;: &#x27;One Comment&#x27;,
&#x27;other&#x27;: &#x27;{} Comments&#x27;})">
</span>
</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane show active" id="pills-diff" role="tabpanel" aria-labelledby="pills-diff-tab">
<div class="pr-diff shadow-sm p-3 mb-4 rounded" style="background: var(--sidebar-bg-color)" v-if="options?.diff">
<pre style="overflow-x: auto"><code v-html="sanitize(fmt?.diff(anonymize(details?.pullRequest?.diff)))"></code></pre>
</div>
</div>
<div class="tab-pane" id="pills-comments" role="tabpanel" aria-labelledby="pills-comments-tab" :class="{&#x27;show active&#x27;:!options?.diff}">
<ul class="pr-comments list-group" v-if="options?.comments">
<li class="pr-comment list-group-item" v-for="(comment, index) in details?.pullRequest?.comments">
<div class="d-flex w-100 justify-content-between flex-wrap">
<h5 class="mb-1" v-if="options?.username">
@{{ anonymize(comment?.author) }}
</h5>
<small v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></small>
</div>
<p class="mb-1">
<markdown class="pr-comment-body" v-if="options?.body" :content="anonymize(comment?.body)" :options="options" :terms="terms"></markdown>
</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>