Ajout : - e2e/csp_preview.spec.js — apercu CSP strict SANS deployer : le build officiel @alpinejs/csp (e2e/fixtures/alpine.csp.js, 0 eval) est servi a la place de alpine.min.js par interception Playwright ; toute expression que le parseur maison ne digere pas = pageerror (filet). Premiere surface VERTE : library (composant lie, icones SVG via Alpine.effect, recherche ouverte + focalisee, 0 erreur). Changed : - 16 composants x-data="fn()" enregistres via Alpine.data (registre = seule resolution du build CSP, probe « Undefined variable » ; scripts classiques executes pendant le parsing => alpine:init toujours joint) : appState, libraryPage, workspacesPage, editorState, board x4, settings/import/table_view/team_load/trash/workspace/welcome/accounts/ card_detail. - base.html (shell) migre : x-effect document.* -> syncSidebarClass(), $nextTick(arrow) -> initSidebarSort(), window.FlowDeck.* -> fdCreatePage/fdCreateFolder/fdGwRefresh, Object.keys/Math.min/ window.innerWidth dans x-for et :style -> sidebarSections()/ sectionMenuPos() — tout = simple appel de methode. - x-html restants du shell -> x-init + Alpine.effect : icone agent, carte projet, library x3 ; recherche library -> toggleSearch() (Alpine.nextTick) ; openMoveSelected() pour Object.keys en expression. - eslint : 70 warnings -> 0/0 (globals getCsrf depuis A38 ph1, /* exported openCardDetail */ + /* global owner, repo */, 3 ;; residuels). suite **1093/1093** · ruff OK · eslint 0/0 · E2E 3/3 (csp_preview + smoke x2) · docs a jour (ROADMAP ph3 LOT 1, CHANGELOG, WORKLOAD, VERSION)
139 lines
5.6 KiB
HTML
139 lines
5.6 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 »).
|
|
|
|
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>
|