Files
flowdeck/app/templates/card_detail.html
T
bruno 7a38ddd0f6
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
test: A32 TERMINÉ — 6 routes Gitea stubbées + bug prod fd_icon (v7.20.0)
Les 6 dernières routes d'A32 (api.py, gitea) avec stub de transport — zéro
réseau réel :

- _stub_gitea() : stubs manuels sur gitea_client.gitea (create_issue,
  update_issue, update_issue_labels, get_issue, get_issue_comments) avec
  ÉTAT MUTABLE PARTAGÉ — le handler PATCH re-fetch l'issue via get_issue,
  un canevas figé aurait masqué la mise à jour.
- POST /issues : carte INSÉRÉE sur le board (board seedé par endpoint) ;
  PATCH : colonne recalculée sans perdre la carte.
- GET /issues JSON + HTML : ?format=html requis (le segment /html ne fixe pas
  le paramètre, le handler le lit dans la query) ; stub qui lève → 404.
- POST /checklists + POST /checklist-items : lignes vérifiées en base,
  404 sans board ; cleanup (items → checklists).

BUG PROD corrigé (trouvé par le smoke HTML) : card_detail.html utilisait la
macro fd_icon SANS l'importer → UndefinedError → 500 systématique sur
GET /api/issues/...?format=html (seul rendu du template dans le code).
Fix : {% from '_icons.html' import fd_icon %}.

A32 COMPLET : plus aucun router « 0 test » (webhooks, notes, sidebar_config,
github_routes, library, api, dashboard, api_v2 tous couverts).

test_smoke_uncovered.py : 52 tests. suite **1089/1089** · ruff OK · docs à jour
2026-10-01 15:39:21 -04:00

150 lines
6.3 KiB
HTML

{# Card detail modal content — full issue info + comments #}
{% from '_icons.html' import fd_icon %}
<div class="card-detail" x-data="cardDetail()">
<!-- Title -->
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:16px;">
<span style="font-size:24px;">{{ issue.icon }}</span>
<div style="flex:1;">
<h2 style="font-size:20px; font-weight:700; color:var(--text-primary); margin:0;"
contenteditable="true"
@blur="updateField('title', $event.target.textContent)">{{ issue.title }}</h2>
<div style="font-size:12px; color:var(--text-dim); margin-top:4px;">
#{{ issue.id }} · opened by {{ issue.author }} · {{ issue.created }}
</div>
</div>
</div>
<!-- Properties -->
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px;">
<!-- Status -->
<div class="filter-pill" style="cursor:pointer;" @click="toggleStatusMenu()">
<span class="legend-dot" style="width:8px; height:8px; background:{{ issue.status_color }};"></span>
{{ issue.status_label }}
<span style="font-size:10px;">▾</span>
</div>
<!-- Assignee -->
<div class="filter-pill" style="cursor:pointer;">
<span>{{ fd_icon("user",14) }}</span>
{% if issue.assignee %}{{ issue.assignee }}{% else %}Unassigned{% endif %}
<span style="font-size:10px;">▾</span>
</div>
<!-- Deadline -->
<div class="filter-pill" style="cursor:pointer;">
<span>{{ fd_icon("calendar",14) }}</span>
{% if issue.due_date %}{{ issue.due_date }}{% else %}No deadline{% endif %}
</div>
<!-- Labels -->
{% for label in issue.labels %}
<div class="filter-pill">
<span class="legend-dot" style="width:8px; height:8px; background:#{{ label.color }};"></span>
{{ label.name }}
</div>
{% endfor %}
</div>
<!-- Body / Description -->
<div style="margin-bottom:16px; padding:12px; background:var(--bg-secondary); border-radius:6px; min-height:60px;"
contenteditable="true"
@blur="updateField('body', $event.target.innerHTML)">
{% if issue.body %}{{ issue.body }}{% else %}<span style="color:var(--text-dim);">Add description...</span>{% endif %}
</div>
<!-- Checklists -->
{% for cl in checklists %}
<div style="margin-bottom:16px;">
<div style="font-weight:600; font-size:14px; margin-bottom:8px;">☑ {{ cl.title }}</div>
{% for item in cl.items %}
<div style="display:flex; align-items:center; gap:8px; padding:4px 0;">
<input type="checkbox" {% if item.checked %}checked{% endif %}
@change="toggleChecklistItem({{ item.id }}, $event.target.checked)"
style="accent-color:var(--blue); width:16px; height:16px;">
<span style="font-size:13px; {% if item.checked %}text-decoration:line-through; color:var(--text-dim);{% endif %}">
{{ item.content }}
</span>
</div>
{% endfor %}
<button class="kanban-new-card" style="margin-top:4px;"
@click="addChecklistItem({{ cl.id }})">+ Add item</button>
</div>
{% endfor %}
<button class="kanban-new-card" @click="addChecklist()">+ Add checklist</button>
<!-- Comments -->
<div style="border-top:1px solid var(--border); padding-top:16px; margin-top:16px;">
<h3 style="font-size:14px; font-weight:600; margin-bottom:12px;">Comments ({{ comments|length }})</h3>
{% for comment in comments %}
<div style="display:flex; gap:10px; margin-bottom:12px; padding:8px; border-radius:6px;">
<div class="card-avatar" style="width:24px; height:24px; font-size:11px; flex-shrink:0;">
{{ comment.user.login[0] if comment.user else '?' }}
</div>
<div style="flex:1;">
<div style="font-size:12px; font-weight:600; margin-bottom:2px;">
{{ comment.user.login if comment.user else 'Unknown' }}
<span class="text-dim" style="font-weight:400;">· {{ comment.created_at[:10] }}</span>
</div>
<div style="font-size:13px; color:var(--text-primary); line-height:1.5;">
{{ comment.body }}
</div>
</div>
</div>
{% endfor %}
{% if not comments %}
<p class="text-dim" style="font-size:13px;">No comments yet.</p>
{% endif %}
<textarea placeholder="Add a comment..."
style="width:100%; min-height:60px; background:var(--bg-secondary); border:1px solid var(--border);
border-radius:6px; padding:8px; color:var(--text-primary); font-family:inherit; font-size:13px;
resize:vertical; margin-top:8px;"
@keydown.ctrl.enter="addComment($event.target.value); $event.target.value=''"></textarea>
</div>
</div>
<script nonce="{{ csp_nonce() }}">
// ponytail: CSRF helper
function getCsrf() {
const m = document.cookie.match(/csrf_token=([^;]+)/);
return m ? m[1] : '';
}
function cardDetail() {
return {
updateField(field, value) {
console.log('Update', field, ':', value);
// TODO: PATCH /api/issues/{owner}/{repo}/{issue_id}
},
toggleStatusMenu() {
console.log('Toggle status');
},
toggleChecklistItem(id, checked) {
fetch(`/api/checklist-items/${id}?checked=${checked}`, {
method: 'PATCH',
headers: { 'X-CSRF-Token': getCsrf() }
});
},
addChecklistItem(clId) {
const content = prompt('Item name:');
if (content) {
fetch(`/api/checklist-items/${owner}/${repo}/${issue_id}/${clId}?content=${encodeURIComponent(content)}`, {
method: 'POST',
headers: { 'X-CSRF-Token': getCsrf() }
})
.then(() => htmx.ajax('GET', `/api/issues/${owner}/${repo}/${issue_id}?format=html`, { target: '#card-modal-content', swap: 'innerHTML' }));
}
},
addChecklist() {
const title = prompt('Checklist name:') || 'Checklist';
fetch(`/api/checklists/${owner}/${repo}/${issue_id}?title=${encodeURIComponent(title)}`, {
method: 'POST',
headers: { 'X-CSRF-Token': getCsrf() }
})
.then(() => htmx.ajax('GET', `/api/issues/${owner}/${repo}/${issue_id}?format=html`, { target: '#card-modal-content', swap: 'innerHTML' }));
},
addComment(body) {
if (!body.trim()) return;
console.log('Add comment:', body);
// TODO: POST to Gitea API for comments
}
};
}
</script>