Files
flowdeck/static/js/task_db_link.js
T
bruno 1d1cdbd618
FlowDeck CI / test (push) Failing after 3h13m58s
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / docker (push) Skipped
fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient
  loovverture et le redimensionnement inoperants -> cblage direct sur le document.
- Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw,
  clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks.
- database-table-container margin:0 (tableau colle a gauche, marge Library).
- .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px.
- ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
2026-10-05 22:47:36 -04:00

206 lines
9.7 KiB
JavaScript

/* ════════════════════════════════════════════════════════════════════════
* FlowDeck — « Convertir en base de tâches »
*
* Modale de linkage : pour les trois rôles requis (Assigné à / Statut /
* Échéance) on choisit soit une colonne existante compatible, soit on en
* crée une nouvelle avec le bon type. Le serveur fait foi
* (`POST /db/{id}/task-db/api`) : il valide les types, refuse le double
* emploi d'une colonne et applique la limite de 10 bases connectées.
* ══════════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
if (window.__fdTaskDbLinkLoaded) return;
window.__fdTaskDbLinkLoaded = true;
var ROLE_LABELS = { assignee: 'Assigné à', status: 'Statut', due: "Date d'échéance" };
var ROLE_HINTS = {
assignee: 'Colonne « personne » utilisée pour filtrer vos tâches.',
status: 'Colonne statut ou sélection (À faire / En cours / Terminée).',
due: 'Colonne de type date, utilisée par les vues et le calendrier.'
};
var DEFAULT_NAMES = { assignee: 'Assigné à', status: 'Statut', due: "Échéance" };
var TYPE_LABELS = {
person: 'personne', status: 'statut', select: 'sélection', date: 'date',
text: 'texte', number: 'nombre', checkbox: 'case à cocher'
};
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function csrf() {
return (typeof window.getCsrf === 'function') ? window.getCsrf() : '';
}
function api(path, opts) {
opts = opts || {};
var headers = { 'X-CSRF-Token': csrf() };
if (opts.body) headers['Content-Type'] = 'application/json';
return fetch(path, {
method: opts.method || 'GET',
credentials: 'same-origin',
headers: headers,
body: opts.body ? JSON.stringify(opts.body) : undefined
}).then(function (r) {
return r.json().catch(function () { return {}; }).then(function (d) {
if (!r.ok) throw new Error(d.detail || ('Erreur ' + r.status));
return d;
});
});
}
/* ── Modale ─────────────────────────────────────────────────────── */
function roleRow(state, role) {
var selected = state.mapping[role];
var current = state.candidates[role] || [];
// La colonne déjà mappée doit rester sélectionnable même si elle est
// de type compatible mais exclue des candidats (déjà affectée).
var all = state.all_properties || [];
if (selected && !current.some(function (p) { return p.id === selected; })) {
var kept = all.filter(function (p) { return p.id === selected; })[0];
if (kept) current = [kept].concat(current);
}
var h = '<div class="tdb-role" data-role="' + role + '">';
h += '<div class="tdb-role-head"><label>' + esc(ROLE_LABELS[role]) + '</label>';
h += '<span class="tdb-role-hint">' + esc(ROLE_HINTS[role]) + '</span></div>';
h += '<div class="tdb-role-body">';
h += '<select class="tdb-select" data-tdb-role="' + role + '">';
h += '<option value="">— créer une nouvelle colonne —</option>';
current.forEach(function (p) {
h += '<option value="' + p.id + '"' +
(String(selected) === String(p.id) ? ' selected' : '') + '>' +
esc(p.name) + ' (' + esc(TYPE_LABELS[p.prop_type] || p.prop_type) +
')</option>';
});
h += '</select>';
h += '<input class="tdb-new-name" data-tdb-new="' + role + '" type="text" ' +
'placeholder="' + esc(DEFAULT_NAMES[role]) + '" ' +
(selected ? 'style="display:none"' : '') + '>';
h += '</div></div>';
return h;
}
function openModal(collectionId) {
closeModal();
api('/db/' + collectionId + '/task-db/api').then(function (state) {
if (!state.exists) return;
var roles = ['assignee', 'status', 'due'];
var h = '<div class="tdb-overlay" id="tdb-overlay"><div class="tdb-modal">';
h += '<div class="tdb-head"><span>' +
(state.is_task ? 'Base de tâches' : 'Convertir en base de tâches') +
'</span><button class="tdb-close" id="tdb-close">✕</button></div>';
h += '<div class="tdb-intro">Pour alimenter <a href="/my-tasks">My Tasks</a>, ' +
'reliez les trois colonnes ci-dessous. Une colonne absente sera créée ' +
'avec le bon type.</div>';
h += '<div class="tdb-body">';
roles.forEach(function (r) { h += roleRow(state, r); });
h += '</div>';
if (state.sources_count >= state.max_sources) {
h += '<div class="tdb-warn">⚠ Limite de ' + state.max_sources +
' bases connectées atteinte : déconnectez-en une avant.</div>';
}
h += '<div class="tdb-foot">';
if (state.is_task) {
h += '<button class="tdb-btn-danger" id="tdb-disable">' +
'Disconnecter</button>';
}
h += '<span class="tdb-spacer"></span>';
h += '<button class="tdb-btn" id="tdb-cancel">Annuler</button>';
h += '<button class="tdb-btn-primary" id="tdb-save"' +
(state.sources_count >= state.max_sources && !state.is_task
? ' disabled' : '') + '>' +
(state.is_task ? 'Mettre à jour' : 'Convertir') + '</button>';
h += '</div></div></div>';
document.body.insertAdjacentHTML('beforeend', h);
wire(state, collectionId);
}).catch(function (e) { alert(e.message); });
}
function closeModal() {
var ov = document.getElementById('tdb-overlay');
if (ov) ov.remove();
}
function wire(state, collectionId) {
var overlay = document.getElementById('tdb-overlay');
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
document.getElementById('tdb-close').onclick = closeModal;
document.getElementById('tdb-cancel').onclick = closeModal;
// Choisir une colonne masque le champ « nouvelle colonne ».
overlay.addEventListener('change', function (e) {
var role = e.target.dataset.tdbRole;
if (!role) return;
var input = overlay.querySelector('[data-tdb-new="' + role + '"]');
if (input) input.style.display = e.target.value ? 'none' : '';
});
var disable = document.getElementById('tdb-disable');
if (disable) {
disable.onclick = function () {
if (!confirm('Disconnecter cette base de My Tasks ?\n\n' +
'Les tâches restent dans la base.')) return;
api('/db/' + collectionId + '/task-db/api', { method: 'DELETE' })
.then(function () { closeModal(); toast('Base déconnectée de My Tasks'); })
.catch(function (e) { alert(e.message); });
};
}
document.getElementById('tdb-save').onclick = function () {
var mapping = {}, create_missing = {};
['assignee', 'status', 'due'].forEach(function (role) {
var sel = overlay.querySelector('[data-tdb-role="' + role + '"]');
var val = sel ? sel.value : '';
if (val) {
mapping[role] = Number(val);
} else {
var input = overlay.querySelector('[data-tdb-new="' + role + '"]');
create_missing[role] = {
name: (input && input.value.trim()) || DEFAULT_NAMES[role]
};
}
});
api('/db/' + collectionId + '/task-db/api',
{ method: 'POST', body: { mapping: mapping, create_missing: create_missing } })
.then(function (d) {
closeModal();
toast(d.configured
? '✅ Base connectée à My Tasks'
: '⚠ Mapping incomplet — des colonnes manquent');
})
.catch(function (e) { alert(e.message); });
};
}
function toast(msg) {
var t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);' +
'z-index:1200;background:var(--bg-modal,#fff);color:var(--text-primary,#333);' +
'border:1px solid var(--border,#e3e3e0);border-radius:8px;padding:9px 16px;' +
'font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.16)';
document.body.appendChild(t);
setTimeout(function () { t.remove(); }, 3200);
}
/* ── Point d'entrée depuis la barre de la base ──────────────────── */
window.FlowDeckTaskDb = { open: openModal, close: closeModal };
document.addEventListener('click', function (e) {
var trigger = e.target.closest ? e.target.closest('[data-task-db]') : null;
if (!trigger) return;
e.preventDefault();
e.stopPropagation();
var id = trigger.dataset.taskDb;
if (id) openModal(Number(id));
});
})();