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)
169 lines
6.4 KiB
JavaScript
169 lines
6.4 KiB
JavaScript
/* exported setActiveTab, kanbanBoard, filterSystem, sortSystem, newIssueForm, showNewIssue -- appeles depuis les attributs HTML des templates */
|
|
const BD=(()=>{try{const el=document.getElementById('bd-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
|
|
|
|
var owner = BD.owner;
|
|
var repo = BD.repo;
|
|
var initialView = BD.initial_view;
|
|
|
|
// Auto-switch to view from URL param
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (initialView && initialView !== 'kanban') {
|
|
const tab = document.querySelector(`.view-tab[data-view="${initialView}"]`);
|
|
if (tab) tab.click();
|
|
}
|
|
});
|
|
|
|
function setActiveTab(el) {
|
|
document.querySelectorAll('.view-tab').forEach(t => t.classList.remove('active'));
|
|
el.classList.add('active');
|
|
}
|
|
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: kanbanBoard »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('kanbanBoard', kanbanBoard); });
|
|
function kanbanBoard() {
|
|
return {
|
|
collapsedGroups: [],
|
|
toggleGroup(id) {
|
|
const idx = this.collapsedGroups.indexOf(id);
|
|
idx >= 0 ? this.collapsedGroups.splice(idx, 1) : this.collapsedGroups.push(id);
|
|
},
|
|
newCard(groupId, status) {
|
|
document.getElementById('new-issue-form').style.display = 'block';
|
|
document.querySelector('#new-issue-form').__x.$data.status = status;
|
|
},
|
|
newGroup() { console.log('New group'); }
|
|
};
|
|
}
|
|
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: filterSystem »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('filterSystem', filterSystem); });
|
|
function filterSystem() {
|
|
return {
|
|
activeFilters: [],
|
|
statusFilters: [],
|
|
showStatusMenu: false,
|
|
statusOptions: [
|
|
{ value: 'todo', label: 'To-do', color: 'var(--gray)' },
|
|
{ value: 'progress', label: 'In progress', color: 'var(--blue)' },
|
|
{ value: 'done', label: 'Complete', color: 'var(--green)' },
|
|
],
|
|
get statusFilterLabel() {
|
|
return this.statusFilters.length ? this.statusFilters.join(', ') : 'All';
|
|
},
|
|
toggleStatus(val) {
|
|
const idx = this.statusFilters.indexOf(val);
|
|
idx >= 0 ? this.statusFilters.splice(idx, 1) : this.statusFilters.push(val);
|
|
},
|
|
addFilter() {
|
|
const prop = prompt('Filter by property (status, assignee, label):');
|
|
if (!prop) return;
|
|
const val = prompt('Value:');
|
|
if (!val) return;
|
|
this.activeFilters.push({ property: prop, value: val });
|
|
this.refreshView();
|
|
},
|
|
removeFilter(i) { this.activeFilters.splice(i, 1); },
|
|
resetFilters() { this.activeFilters = []; this.statusFilters = []; },
|
|
refreshView() {
|
|
const activeTab = document.querySelector('.view-tab.active');
|
|
if (activeTab) activeTab.click();
|
|
}
|
|
};
|
|
}
|
|
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: sortSystem »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('sortSystem', sortSystem); });
|
|
function sortSystem() {
|
|
return {
|
|
sorts: [],
|
|
showSortPanel: false,
|
|
addSort() {
|
|
const field = prompt('Sort by (name, status, assignee, deadline):');
|
|
if (!field) return;
|
|
this.sorts.push({ field, dir: 'asc' });
|
|
this.applySorts();
|
|
},
|
|
removeSort(i) { this.sorts.splice(i, 1); this.applySorts(); },
|
|
toggleDir(i) { this.sorts[i].dir = this.sorts[i].dir === 'asc' ? 'desc' : 'asc'; this.applySorts(); },
|
|
clearSorts() { this.sorts = []; this.applySorts(); },
|
|
applySorts() {
|
|
const activeTab = document.querySelector('.view-tab.active');
|
|
const url = new URL(activeTab.getAttribute('hx-get'), window.location.origin);
|
|
this.sorts.forEach(s => url.searchParams.append('sort', s.field + ':' + s.dir));
|
|
htmx.ajax('GET', url.pathname + url.search, { target: '#view-content', swap: 'innerHTML' });
|
|
}
|
|
};
|
|
}
|
|
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: newIssueForm »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('newIssueForm', newIssueForm); });
|
|
function newIssueForm() {
|
|
return {
|
|
title: '', status: 'todo',
|
|
create() {
|
|
if (!this.title.trim()) return;
|
|
const csrf = getCsrf();
|
|
fetch(`/api/issues/${owner}/${repo}?title=${encodeURIComponent(this.title)}&labels=${this.status}`, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': csrf }
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
this.title = '';
|
|
this.hide();
|
|
// Refresh current view
|
|
const activeTab = document.querySelector('.view-tab.active');
|
|
if (activeTab) activeTab.click();
|
|
});
|
|
},
|
|
hide() { document.getElementById('new-issue-form').style.display = 'none'; },
|
|
show() { document.getElementById('new-issue-form').style.display = 'block'; }
|
|
};
|
|
}
|
|
|
|
function showNewIssue() {
|
|
const form = document.getElementById('new-issue-form');
|
|
form.style.display = form.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
// Init SortableJS after HTMX swaps
|
|
document.addEventListener('htmx:afterSwap', function(evt) {
|
|
if (evt.target.id === 'view-content') {
|
|
document.querySelectorAll('.kanban-cards').forEach(el => {
|
|
if (el._sortable) el._sortable.destroy();
|
|
el._sortable = new Sortable(el, {
|
|
group: 'kanban',
|
|
animation: 200,
|
|
ghostClass: 'sortable-ghost',
|
|
dragClass: 'sortable-drag',
|
|
onEnd: function(evt) {
|
|
const cardId = evt.item.dataset.cardId;
|
|
const toStatus = evt.to.dataset.status;
|
|
const csrf = getCsrf();
|
|
fetch(`/api/move?owner=${owner}&repo=${repo}&issue_id=${cardId}&column=${toStatus}`, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': csrf }
|
|
})
|
|
.then(() => {
|
|
// ponytail: refresh current view after move
|
|
const activeTab = document.querySelector('.view-tab.active');
|
|
if (activeTab) activeTab.click();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|