Files
flowdeck/static/js/board.js
T
bruno 6d7af3fb64
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / test (push) Successful in 15m9s
feat: éditeur visuel d'automations (pipeline steps) + fix CSP multi-instructions (v7.45.0)
Added — Settings → Automations, pipeline visuel (API steps v7.0) :
- Cartes ordinees (trigger/condition/delay/action) : resume + edition
  TYPÉE par kind/type (datalist evenements, 7 ops, 8 types d'action avec
  leurs champs reels), ajout/edition/suppression/haut-bas via
  POST/PUT/DELETE /workspace/automations[/steps]/...
- ✨ Convertir le JSON en pipeline (legacy → steps ordonnes) ; textareas
  JSON masques des qu'un step existe.
- Gate E2E « editeur visuel de steps » : creation → edition → ajout →
  carte « Action · webhook » sous CSP reel.

Fixed (trouve par le gate) — 51 expressions Alpine MULTI-INSTRUCTIONS
(`a=1; b()`) = interdites par le parseur CSP (une seule expression par
directive ; ';' = token inattendu) — INVISIBLE pour le scan par tokens :
- Conversion en methodes dans 11 fichiers : nav settings x8 (navTo),
  menu section base x7 (closeAndSetCount/Move/...), parts+editeur x13
  (setSharePerm, more*, markAndSave...), breadcrumb x5 (hover*/goClose),
  library/local x6 (menus popup), board x3 (pickStatus/...), ctx-menu x2
  (addTagAndClear), agent/card/gitea/workspaces x6.
- Scanner dedie scan_semi (inventaire ';' hors chaines) ajoute au lot.

Verifs : 39 templates Jinja parse OK · scan expressions = 0 incompatible
(4 faux positifs en chaines) · **E2E 8/8** · suite **1094/1094** ·
ruff OK · docs a jour
2026-10-02 16:56:29 -04:00

172 lines
6.6 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 = []; },
removeFilterAndRefresh(i) { this.removeFilter(i); this.refreshView(); },
resetFiltersAndRefresh() { this.resetFilters(); this.refreshView(); },
pickStatus(v) { this.toggleStatus(v); this.showStatusMenu = false; this.refreshView(); },
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();
});
}
});
});
}
});