feat: Fusion bloc 1 — barre d'apps commune (ACC-1.1, #17) et références croisées external_ref (ACC-1.3, #18)
FlowDeck CI / lint (push) Failing after 2m33s
FlowDeck CI / test (push) Failing after 30m3s
FlowDeck CI / docker (push) Skipped

This commit is contained in:
Spark
2026-10-20 23:17:18 -04:00
parent 12beab01c9
commit d8548eede9
16 changed files with 845 additions and 4 deletions
+65
View File
@@ -0,0 +1,65 @@
/* Références croisées des pages — Fusion ACC-1.3 (Flowdeck).
* Câble la section « Liens » rendue côté serveur dans l'éditeur de page :
* ajout par collage d'URL profonde (POST /api/pages/<id>/refs) et retrait
* (DELETE /api/refs/<id>), jeton CSRF lu dans le cookie comme le reste de
* l'app. Après chaque mutation, la page est rechargée — l'état affiché
* reste celui du serveur, seule source de vérité.
*/
(function () {
"use strict";
function csrf() {
var m = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/);
return m ? decodeURIComponent(m[1]) : "";
}
function wire(section) {
var ownerType = section.getAttribute("data-owner-type");
var ownerId = section.getAttribute("data-owner-id");
var base = ownerType === "flowdeck.project"
? "/api/collections/" + ownerId + "/refs"
: "/api/pages/" + ownerId + "/refs";
var erreur = section.querySelector("[data-refs-erreur]");
function showError(msg) {
if (erreur) {
erreur.textContent = msg;
erreur.hidden = false;
}
}
section.querySelectorAll("[data-ref-del]").forEach(function (btn) {
btn.addEventListener("click", function () {
fetch("/api/refs/" + btn.getAttribute("data-ref-del"), {
method: "DELETE",
credentials: "same-origin",
headers: { "X-CSRF-Token": csrf() }
}).then(function (r) {
if (r.ok) { window.location.reload(); }
else { r.json().then(function (d) { showError(d.detail || "HTTP " + r.status); }); }
}).catch(function () { showError("Réseau indisponible"); });
});
});
var form = section.querySelector("[data-refs-form]");
if (form) {
form.addEventListener("submit", function (ev) {
ev.preventDefault();
var inputs = form.querySelectorAll("input");
fetch(base, {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf() },
body: JSON.stringify({ url: inputs[0].value.trim(), label: inputs[1].value.trim() || null })
}).then(function (r) {
if (r.ok) { window.location.reload(); }
else { r.json().then(function (d) { showError(d.detail || "HTTP " + r.status); }); }
}).catch(function () { showError("Réseau indisponible"); });
});
}
}
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("[data-refs]").forEach(wire);
});
})();
+56
View File
@@ -0,0 +1,56 @@
/* Barre d'apps commune dracodev — comportement (v0.1.0, ACC-0.2)
*
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
* (patron validé par le prototype — les onclick du HTML généré sont
* proscrits dans tout le programme).
*
* Configuration attendue avant le chargement :
* window.DRACO_BAR_CONFIG = {
* current: "tiktak", // app courante
* domain: "pro", // domaine actif
* counts: { tiktak: 3, kanbante: 3, alertes: 3, spark: 0 },
* onAction: function (act, el) { … } // l'app relaie les actions
* } // qu'elle ne connaît pas
* Actions émises (data-act) : rail, palette, domaine (data-dom),
* alertes, spark, menu, rappel-entete. L'app intégratrice décide de
* chacune (ouvrir la palette, le panneau Spark, changer de domaine…).
*/
(function () {
"use strict";
var cfg = window.DRACO_BAR_CONFIG || {};
function setCounts(counts) {
Object.keys(counts || {}).forEach(function (k) {
var b = document.querySelector('.draco-badge[data-badge="' + k + '"]');
if (!b) return;
b.textContent = counts[k];
b.hidden = !(counts[k] > 0);
});
}
function setCurrent(app) {
document.querySelectorAll(".draco-apptab").forEach(function (t) {
t.dataset.active = t.dataset.app === app ? "true" : "false";
});
}
function setDomain(dom) {
document.querySelectorAll('.draco-domaines [data-dom]').forEach(function (b) {
b.dataset.on = b.dataset.dom === dom ? "true" : "false";
});
}
document.addEventListener("click", function (ev) {
var el = ev.target.closest("[data-act]");
if (!el) return;
var act = el.dataset.act;
if (act === "domaine") setDomain(el.dataset.dom);
if (typeof cfg.onAction === "function") cfg.onAction(act, el, ev);
else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
});
/* API publique : l'app met les compteurs à jour sans recharger. */
window.DRACO_BAR = { setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain };
if (cfg.current) setCurrent(cfg.current);
if (cfg.domain) setDomain(cfg.domain);
if (cfg.counts) setCounts(cfg.counts);
})();