- 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).
206 lines
9.7 KiB
JavaScript
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
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));
|
|
});
|
|
})();
|