Issues: per-project numbers, statuses (open, accepted, rejected, withdrawn, duplicate), feature links, edits, and a timeline of comments and events. From an issue, collaborators create or update a feature with the revision tied to the issue and an option to accept it. Comments: general and line-anchored threads on features, one discussion thread per issue, edit history, tombstone deletes, resolve, and promoting a feature thread to an issue. Line threads attach to top-level Markdown blocks and go outdated only when the text changes. HTMX swaps threads in place with focus management; every action also works as a plain form post. Markdown headings are re-levelled so user text never skips heading levels on the page. The example flow from concept.md (steps 1-7) is covered end to end in tests/test_example_flow.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
5.2 KiB
HTML
114 lines
5.2 KiB
HTML
{% extends "projects/base.html" %}
|
|
|
|
{% block page_title %}{{ issue.key }} {{ issue.title }}{% endblock %}
|
|
|
|
{% block project_content %}
|
|
<h1><span class="muted">{{ issue.key }}</span> {{ issue.title }}</h1>
|
|
<p>
|
|
<strong>{{ issue.get_status_display }}</strong>{% if issue.duplicate_of %} duplicate of <a href="{% url 'issues:detail' org_slug=project.org.slug project_slug=project.slug number=issue.duplicate_of.number %}">{{ issue.duplicate_of.key }} {{ issue.duplicate_of.title }}</a>{% endif %}.
|
|
Opened by {{ issue.author.username|default:"deleted user" }}
|
|
on <time datetime="{{ issue.created_at|date:'c' }}">{{ issue.created_at|date:"j M Y" }}</time>.
|
|
{% if promoted_url %}Started as <a href="{{ promoted_url }}">a comment thread on {{ promoted_from.feature.key }}</a>.{% endif %}
|
|
</p>
|
|
{% if can_edit_issue %}
|
|
<p><a href="{% url 'issues:edit' org_slug=project.org.slug project_slug=project.slug number=issue.number %}">Edit issue</a></p>
|
|
{% endif %}
|
|
|
|
<section aria-labelledby="body-heading">
|
|
<h2 id="body-heading" class="visually-hidden">Description</h2>
|
|
<div class="prose">
|
|
{% if issue.body %}{{ body }}{% else %}<p class="muted">No description.</p>{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<section id="features" aria-labelledby="features-heading">
|
|
<h2 id="features-heading">Features</h2>
|
|
{% if features %}
|
|
<ul>
|
|
{% for feature in features %}
|
|
<li>
|
|
<a href="{% url 'features:detail' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key %}">{{ feature.key }} {{ feature.title }}</a>
|
|
{% if access.can_edit %}
|
|
<form method="post" action="{% url 'issues:features' org_slug=project.org.slug project_slug=project.slug number=issue.number %}" class="inline">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="unlink">
|
|
<input type="hidden" name="feature" value="{{ feature.pk }}">
|
|
<button type="submit">Unlink <span class="visually-hidden">{{ feature.key }}</span></button>
|
|
</form>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">Not linked to any feature.</p>
|
|
{% endif %}
|
|
|
|
{% if access.can_edit %}
|
|
{% if link_form.fields.feature.queryset.exists %}
|
|
<form method="post" action="{% url 'issues:features' org_slug=project.org.slug project_slug=project.slug number=issue.number %}">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="action" value="link">
|
|
{{ link_form }}
|
|
<button type="submit">Link feature</button>
|
|
</form>
|
|
{% endif %}
|
|
<p>Change features for this issue. The revision is tied to {{ issue.key }}{% if issue.is_open %}, and you can accept the issue as you save{% endif %}.</p>
|
|
<ul>
|
|
<li><a href="{% url 'features:create' org_slug=project.org.slug project_slug=project.slug %}?issue={{ issue.number }}">Create a feature from {{ issue.key }}</a></li>
|
|
{% for feature in features %}
|
|
<li><a href="{% url 'features:edit' org_slug=project.org.slug project_slug=project.slug feature_key=feature.key %}?issue={{ issue.number }}">Update {{ feature.key }} for {{ issue.key }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{% if duplicates %}
|
|
<section aria-labelledby="duplicates-heading">
|
|
<h2 id="duplicates-heading">Duplicates</h2>
|
|
<ul>
|
|
{% for dup in duplicates %}
|
|
<li><a href="{% url 'issues:detail' org_slug=project.org.slug project_slug=project.slug number=dup.number %}">{{ dup.key }} {{ dup.title }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if access.can_edit or can_withdraw %}
|
|
{% url 'issues:status' org_slug=project.org.slug project_slug=project.slug number=issue.number as status_url %}
|
|
<section id="decide" aria-labelledby="decide-heading">
|
|
<h2 id="decide-heading">Decision</h2>
|
|
{% if issue.is_open %}
|
|
<form method="post" action="{{ status_url }}">
|
|
{% csrf_token %}
|
|
{% if access.can_edit %}
|
|
<button type="submit" name="status" value="accepted">Accept</button>
|
|
<button type="submit" name="status" value="rejected">Reject</button>
|
|
{% endif %}
|
|
{% if can_withdraw %}
|
|
<button type="submit" name="status" value="withdrawn">Withdraw</button>
|
|
{% endif %}
|
|
</form>
|
|
{% if access.can_edit %}
|
|
<form method="post" action="{{ status_url }}">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="status" value="duplicate">
|
|
{{ duplicate_form }}
|
|
<button type="submit">Mark as duplicate</button>
|
|
</form>
|
|
{% endif %}
|
|
{% elif access.can_edit or issue.status == "withdrawn" %}
|
|
<form method="post" action="{{ status_url }}">
|
|
{% csrf_token %}
|
|
<button type="submit" name="status" value="open">Reopen</button>
|
|
</form>
|
|
{% else %}
|
|
<p class="muted">This issue is closed.</p>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section aria-labelledby="discussion-heading">
|
|
<h2 id="discussion-heading">Discussion</h2>
|
|
{% include "issues/_timeline.html" %}
|
|
</section>
|
|
{% endblock %}
|