intent/templates/features/detail.html
Alec Perkins f6b7374be7 Add feature files, image pins and implementations (Phase 4)
Files: prototype and documentation files on features, with versions.
Adding, replacing or removing a file is a feature revision with a
change note. Types come from the extension; uploads are capped at
25 MB and stored under opaque paths. Every file URL checks project
access, then redirects to a short-lived signed URL on the bucket, or
streams with nosniff and a CSP sandbox when no bucket is configured.
HTML prototypes run in a sandboxed iframe without same-origin access.

Threads: whole-file threads and pins at a point on an image, drawn as
an SVG overlay in percentages. Pins are placed with number fields, or
by clicking the image with JS.

Implementations: name, link and status, plus a per-feature status grid
stamped with the feature revision, marked behind when the feature
changes, and confirmable without changing the status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:15:30 -04:00

201 lines
8.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "projects/base.html" %}
{% block page_title %}{{ feature.key }} {{ feature.title }}{% endblock %}
{% block project_content %}
{% include "features/_header.html" %}
{% if feature.status == "withdrawn" %}
<p role="note"><strong>Withdrawn.</strong> This feature is no longer planned.</p>
{% elif feature.status == "superseded" %}
<p role="note"><strong>Superseded</strong> by <a href="{% url 'features:detail' org_slug=project.org.slug project_slug=project.slug feature_key=feature.superseded_by.key %}">{{ feature.superseded_by.key }} {{ feature.superseded_by.title }}</a>.</p>
{% endif %}
<dl>
<dt>Status</dt>
<dd>{{ feature.get_status_display }}</dd>
<dt>Revision</dt>
<dd>
{{ feature.current_revision }}
(<a href="{% url 'features:history' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key %}">history</a>)
</dd>
{% if latest %}
<dt>Last change</dt>
<dd>
<time datetime="{{ latest.created_at|date:'c' }}">{{ latest.created_at|date:"j M Y" }}</time>
by {{ latest.author.username|default:"unknown" }}: {{ latest.note }}
</dd>
{% endif %}
</dl>
{% if access.can_edit %}
<ul>
<li><a href="{% url 'features:edit' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key %}">Edit</a></li>
<li><a href="{% url 'features:create' org_slug=project.org.slug project_slug=project.slug %}?parent={{ feature.key }}">Add a sub-feature</a></li>
</ul>
{% endif %}
<section aria-labelledby="description-heading">
<h2 id="description-heading" class="visually-hidden">Description</h2>
{% if blocks %}
{% url 'features:thread_create' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key as thread_create_url %}
<div class="prose">
{% for block, threads in blocks %}
{# Each block of the description can carry its own threads, like lines in a code review. #}
<div class="block" id="{{ block.anchor }}">
{{ block.html }}
{% for thread in threads %}
{% include "comments/_thread.html" %}
{% endfor %}
{% if access.can_contribute %}
<details class="line-comment">
<summary>Comment<span class="visually-hidden"> on lines {{ block.start }}–{{ block.end }}</span></summary>
<form method="post" action="{{ thread_create_url }}">
{% csrf_token %}
<input type="hidden" name="revision" value="{{ feature.current_revision }}">
<input type="hidden" name="line_start" value="{{ block.start }}">
<input type="hidden" name="line_end" value="{{ block.end }}">
<label for="body-{{ block.anchor }}">Comment on lines {{ block.start }}–{{ block.end }}</label>
<textarea id="body-{{ block.anchor }}" name="body" rows="3" required></textarea>
<button type="submit">Comment</button>
</form>
</details>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="muted">No description yet.</p>
{% endif %}
</section>
{% if children %}
<section aria-labelledby="children-heading">
<h2 id="children-heading">Sub-features</h2>
<ul>
{% for child in children %}
<li>
<a href="{% url 'features:detail' org_slug=project.org.slug project_slug=project.slug feature_key=child.key %}">{{ child.key }} {{ child.title }}</a>
{% if child.status != "active" %}<span class="muted">({{ child.get_status_display|lower }})</span>{% endif %}
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{% if supersedes %}
<section aria-labelledby="supersedes-heading">
<h2 id="supersedes-heading">Replaces</h2>
<ul>
{% for old in supersedes %}
<li><a href="{% url 'features:detail' org_slug=project.org.slug project_slug=project.slug feature_key=old.key %}">{{ old.key }} {{ old.title }}</a></li>
{% endfor %}
</ul>
</section>
{% endif %}
<section id="files" aria-labelledby="files-heading">
<h2 id="files-heading">Files</h2>
{% if files %}
<ul>
{% for v in files %}
<li>
<a href="{% url 'files:detail' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key file_id=v.file_id %}">{{ v.original_name }}</a>
<span class="muted">({{ v.file.get_kind_display|lower }}, version {{ v.version }}{% if v.thread_count %}, {{ v.thread_count }} open thread{{ v.thread_count|pluralize }}{% endif %})</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="muted">No files.</p>
{% endif %}
{% if access.can_edit %}
<details>
<summary>Add a file</summary>
<form method="post" enctype="multipart/form-data" action="{% url 'files:add' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key %}">
{% csrf_token %}
{{ add_file_form.as_p }}
<button type="submit">Add file</button>
</form>
</details>
{% endif %}
</section>
<section id="implementations" aria-labelledby="implementations-heading">
<h2 id="implementations-heading">Implementations</h2>
{% if implementations %}
<table>
<thead>
<tr>
<th scope="col">Implementation</th>
<th scope="col">Status</th>
<th scope="col">As of revision</th>
</tr>
</thead>
<tbody>
{% for row in implementations %}
<tr>
<th scope="row">
<a href="{% url 'implementations:detail' org_slug=project.org.slug project_slug=project.slug impl_id=row.impl.pk %}#{{ feature.key }}">{{ row.impl.name }}</a>
{% if row.impl.status == "archived" %}<span class="muted">(archived)</span>{% endif %}
</th>
<td>{{ row.label }}</td>
<td>
{% if row.revision %}{{ row.revision }}{% if row.behind %} <strong>behind</strong> <span class="visually-hidden">the current revision, {{ feature.current_revision }}</span>{% endif %}{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="muted">No implementations yet. <a href="{% url 'implementations:list' org_slug=project.org.slug project_slug=project.slug %}">Add one</a>.</p>
{% endif %}
</section>
<section id="issues" aria-labelledby="issues-heading">
<h2 id="issues-heading">Issues</h2>
{% if issues %}
<ul>
{% for issue in issues %}
<li>
<a href="{% url 'issues:detail' org_slug=project.org.slug project_slug=project.slug number=issue.number %}">{{ issue.key }} {{ issue.title }}</a>
<span class="muted">({{ issue.get_status_display|lower }})</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="muted">No issues linked.</p>
{% endif %}
{% if access.can_contribute %}
<p><a href="{% url 'issues:create' org_slug=project.org.slug project_slug=project.slug %}?feature={{ feature.key }}">Open an issue about {{ feature.key }}</a></p>
{% endif %}
</section>
<section id="discussion" aria-labelledby="discussion-heading">
<h2 id="discussion-heading">Discussion</h2>
{% for thread in general_threads %}
{% include "comments/_thread.html" %}
{% empty %}
<p class="muted">No comments yet.</p>
{% endfor %}
{% if access.can_contribute %}
<form method="post" action="{% url 'features:thread_create' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key %}">
{% csrf_token %}
<label for="new-thread">Start a thread about {{ feature.key }}</label>
<textarea id="new-thread" name="body" rows="4" required></textarea>
<p class="muted">Markdown. To comment on part of the description, use the Comment button below that part.</p>
<button type="submit">Comment</button>
</form>
{% elif not user.is_authenticated and project.open_contribution %}
<p><a href="{% url 'account_login' %}?next={{ request.path|urlencode }}">Sign in</a> to join the discussion.</p>
{% endif %}
</section>
{% if outdated_threads %}
<section aria-labelledby="outdated-heading">
<h2 id="outdated-heading">Comments on earlier revisions</h2>
{% for thread in outdated_threads %}
{% include "comments/_thread.html" %}
{% endfor %}
</section>
{% endif %}
{% endblock %}