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).
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* 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));
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user