Files
flowdeck/app/templates/table_view.html
T
bruno afbc236cc2
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m16s
fix: navigation partielle Alpine sans course + scripts de page idempotents (v7.45.2)
- app.js : x-ignore sur .main-wrapper au swap fdLoad (nœud remplacé
  uniquement), démontage unique quand tous les <script src> ont exécuté
  (load/error + filet 4s) via Alpine.initTree idempotent (_x_marker) —
  fini les cascades « Undefined variable » (éditeur) et « reading 'has' »
  ($store.fdCtx) : la zone s'initialisait avant composants et stores.
- 5 scripts de page gardés contre la ré-exécution (SyntaxError
  « Identifier 'LW' has already been declared » à la 2e visite
  partielle) : local_workspace, board, settings, database_table,
  page_editor_realtime.
- if (window.Alpine) → Alpine.data immédiat sinon listener alpine:init
  (déjà passé sur swap) : 8 scripts + 7 templates inline.
- app.css : purge des 7 blocs @font-face Inter orphelins (fichiers
  inexistants → 302 HTML → « Failed to decode downloaded font »).
- Gates : e2e/regression_editor_mount + e2e/regression_partial_nav
  (7 pages x complet/1er/2e passage) + regression_logout_dnd + smoke
  = 6/6 verts sur l'image rebuildée · pytest 1094 passed · ruff OK ·
  OpenAPI 7.45.2.
2026-10-02 21:29:46 -04:00

140 lines
5.7 KiB
HTML

{% from '_icons.html' import fd_icon %}
<!-- Table View — Notion style -->
<div id="view-content" x-data="tableView()">
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th @click="sortBy('name')" style="min-width: 250px;">
<span class="th-icon">Aa</span> Name
<span x-show="sortField === 'name'" x-text="sortDir === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('status')">
<span class="th-icon">{{ fd_icon("settings",14) }}</span> Status
<span x-show="sortField === 'status'" x-text="sortDir === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('assignee')">
<span class="th-icon">{{ fd_icon("user",14) }}</span> Assign
<span x-show="sortField === 'assignee'" x-text="sortDir === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('deadline')">
<span class="th-icon">{{ fd_icon("calendar",14) }}</span> Deadline
<span x-show="sortField === 'deadline'" x-text="sortDir === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('team')">
<span class="th-icon">{{ fd_icon("users",14) }}</span> Team
<span x-show="sortField === 'team'" x-text="sortDir === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('keywords')">
<span class="th-icon">{{ fd_icon("tag",14) }}</span> AI keywords
<span x-show="sortField === 'keywords'" x-text="sortDir === 'asc' ? '↑' : '↓'"></span>
</th>
</tr>
</thead>
<tbody>
{% for group_name, items in grouped_cards.items() %}
<!-- Group header -->
<tr class="table-group-header" @click="toggleGroup('{{ group_name }}')" style="cursor:pointer;">
<td colspan="6" style="font-weight:600; padding:10px 12px; background:var(--bg-hover); border-bottom:1px solid var(--border);">
<span class="group-chevron" style="font-size:10px; margin-right:6px;">▶</span>
{{ group_name }} <span style="color:var(--text-dim); font-weight:400;">{{ items|length }}</span>
</td>
</tr>
{% for card in items %}
<tr class="table-row" data-card-id="{{ card.id }}" x-show="!collapsedGroups.includes('{{ group_name }}')"
@click="openCard('{{ card.id }}')" style="cursor:pointer;">
<td>
<div class="table-name-cell">
<span class="item-icon">{{ card.icon }}</span>
<span>{{ card.title }}</span>
</div>
</td>
<td>
<span class="card-tag" style="background:{{ card.status_color }}20; color:{{ card.status_color }};">
{{ card.status_label }}
</span>
</td>
<td>
{% if card.assignee %}
<div style="display:flex; align-items:center; gap:6px;">
<div class="card-avatar" style="width:20px; height:20px; font-size:10px;">{{ card.assignee[0] }}</div>
{{ card.assignee }}
</div>
{% else %}
<span class="text-dim">—</span>
{% endif %}
</td>
<td>
{% if card.due_date %}
<span>{{ card.due_date }}</span>
{% else %}
<span class="text-dim">—</span>
{% endif %}
</td>
<td>
{% if card.tag %}
<span class="card-tag" style="background:{{ card.tag_color }}20; color:{{ card.tag_color }};">
{{ card.tag }}
</span>
{% else %}
<span class="text-dim">—</span>
{% endif %}
</td>
<td>
{% if card.keywords %}
<div style="display:flex; gap:4px; flex-wrap:wrap;">
{% for kw in card.keywords %}
<span class="card-tag" style="background:var(--bg-tertiary); color:var(--text-secondary);">{{ kw }}</span>
{% endfor %}
</div>
{% else %}
<span class="text-dim">—</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<script nonce="{{ csp_nonce() }}">
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
// globales window — probe « Undefined variable: tableView »).
if (window.Alpine) { Alpine.data('tableView', tableView); }
else document.addEventListener('alpine:init', function () { Alpine.data('tableView', tableView); });
function tableView() {
return {
sortField: '',
sortDir: 'asc',
collapsedGroups: [],
sortBy(field) {
if (this.sortField === field) {
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortDir = 'asc';
}
// Re-fetch with sort params via HTMX
const url = new URL(window.location);
url.searchParams.set('sort', field);
url.searchParams.set('dir', this.sortDir);
htmx.ajax('GET', url.pathname + '/view/table' + url.search, { target: '#view-content', swap: 'outerHTML' });
},
toggleGroup(name) {
const idx = this.collapsedGroups.indexOf(name);
if (idx >= 0) this.collapsedGroups.splice(idx, 1);
else this.collapsedGroups.push(name);
},
openCard(id) {
htmx.ajax('GET', `/api/issues/${owner}/${repo}/${id}?format=html`, { target: '#card-modal-content', swap: 'innerHTML' });
document.getElementById('card-modal').style.display = 'flex';
document.getElementById('card-modal-title').textContent = '#' + id;
}
};
}
</script>