- Les 4 peeks (library.js, local_workspace.js, my_tasks.js, database_table.js)
chargent /pages/{id}?embed=1 : le panneau ne montre plus que le document en
mode edition, sans sidebar ni barre (regression v7.49.0). Le cadre du
panneau (close/full/resize partages fdWirePeekResize) est inchange.
- Calendrier My Tasks : le handler testait e.target.dataset.openRow mais les
boutons .mt-cal-open ne contiennent que leur <svg> -> clic sur l'icone =
e.target = <svg>, rien ne partait. cause racine corrigee par
t.closest('[data-open-row]') : un seul chemin tableau/calendrier/Kanban.
- Kanban My Tasks : meme bouton Open ajoute sur les tuiles (openButton(t),
overlay absolu revele au survol de la carte .mt-card).
- Menu contextuel global « Open in side peek » : etait un toast d'erreur ;
route maintenant vers le peek de la page courante (_libData.openPeekById /
_wsData.openSidePeek), repli nouvel onglet ailleurs.
- /pages/{id}?embed=1 sur une page de base (content_format=collection)
rendait un editeur de blocs vide : le template collection est garde en
embed (body.embed-mode retire sidebar+barre, le tableau reste). Bouton
pleine page des peeks de bases : navigue via dataset.page (URL propre)
au lieu du src ?embed=1.
- Tests : garde de coherence inversee (?embed=1 exige sur les 4 fichiers),
+3 nouveaux (closest, tuiles Kanban, collection embed). Fix d'un test
pre-existant casse (assertion panel.style.display, reecriture vanilla JS
v7.49.0). Suite 1264 passed / 0 failed (-n auto) + ruff 0.
- Bump VERSION/main.py 7.50.0, CHANGELOG, WORKLOAD, OpenAPI regenere.
977 lines
44 KiB
JavaScript
977 lines
44 KiB
JavaScript
/* ════════════════════════════════════════════════════════════════════════
|
||
* FlowDeck — My Tasks (tableau de bord façon Notion)
|
||
*
|
||
* Trois vues sur les mêmes tâches normalisées par `/my-tasks/api` :
|
||
* • tableau — liste dense, statut / échéance éditables au clic ;
|
||
* • kanban — colonnes = options de statut de la base d'origine, glisser
|
||
* d'une colonne à l'autre écrit le statut ;
|
||
* • calendrier — mois courant, tâches placées sur leur date d'échéance.
|
||
*
|
||
* Tout passe par le mapping enregistré côté serveur
|
||
* (`collections.task_*_prop`) : le front envoie un champ (`status`, `due`,
|
||
* `title`), jamais un identifiant de colonne.
|
||
* ══════════════════════════════════════════════════════════════════════ */
|
||
/* Le tableau de bord est monté par `flowDeckMyTasks()` pour **chaque** noeud
|
||
* `#my-tasks-app` : la navigation interne de `app.js` (htmx) remplace
|
||
* `.main-wrapper` sans recharger le document — le <script> est bien rejoué,
|
||
* mais son garde global faisait sortir le IIFE et la nouvelle coquille restait
|
||
* vide jusqu'au hard refresh. Le drapeau est désormais porté par l'élément
|
||
* (`__fdMounted`), pas par `window`.
|
||
*
|
||
* Filet supplémentaire : une coquille montée mais **restée vide** est le
|
||
* symptôme d'un swap interrompu. Plutôt que d'afficher du vide, on remonte
|
||
* (les delegations d'événements, elles, ne sont posées qu'une fois par
|
||
* noeud grâce à `__fdWired`). */
|
||
function flowDeckMyTasks() {
|
||
'use strict';
|
||
|
||
var app = document.getElementById('my-tasks-app');
|
||
if (!app) return;
|
||
if (app.__fdMounted && app.innerHTML.trim()) return;
|
||
app.__fdMounted = true;
|
||
var wire = !app.__fdWired; // écoutes posées une seule fois par noeud
|
||
app.__fdWired = true;
|
||
|
||
var cfg = {};
|
||
try { cfg = JSON.parse(app.getAttribute('data-config') || '{}'); } catch (e) { cfg = {}; }
|
||
|
||
var state = {
|
||
view: cfg.view || 'table',
|
||
due: (cfg.filters && cfg.filters.due) || 'all',
|
||
sort: (cfg.filters && cfg.filters.sort) || 'due',
|
||
hide_done: !!(cfg.filters && cfg.filters.hide_done),
|
||
sources: cfg.sources || [],
|
||
maxSources: cfg.maxSources || 10,
|
||
tasks: [],
|
||
month: null,
|
||
calView: 'month',
|
||
calDay: null
|
||
};
|
||
|
||
var MONTHS = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet',
|
||
'août', 'septembre', 'octobre', 'novembre', 'décembre'];
|
||
var DAYS = ['lu', 'ma', 'me', 'je', 've', 'sa', 'di'];
|
||
|
||
function csrf() {
|
||
return (typeof window.getCsrf === 'function') ? window.getCsrf() : '';
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
|
||
// `collection_pages.icon` vaut la chaine sentinelle "file" pour une ligne
|
||
// sans icône (héritage de l'import, cf. `database_table.js`). L'afficher
|
||
// telle quel collait le mot "file" devant chaque titre.
|
||
function rowIcon(icon) {
|
||
return (!icon || icon === 'file') ? '📄' : icon;
|
||
}
|
||
|
||
// Un identifiant de ligne de base n'est PAS un identifiant de page :
|
||
// /db/pages/{id}/open/api renvoie la page de contenu (créée à la volée)
|
||
// qui porte les blocs de la tâche. On ne navigue JAMAIS vers
|
||
// /pages/{rowId} : cet id désigne une page sans rapport (cf.
|
||
// database_table.js) — d'où le panneau latéral, comme dans Library.
|
||
var PEEK_SVG_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
|
||
'stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" ' +
|
||
'x2="18" y2="18"/></svg>';
|
||
var PEEK_SVG_FULL = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
|
||
'stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/>' +
|
||
'<polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
|
||
|
||
// Le panneau vit HORS de `#my-tasks-app` : `render()` remplace le contenu
|
||
// de l'app à chaque filtre, le panneau doit survivre.
|
||
function ensurePeek() {
|
||
var p = document.getElementById('mt-peek');
|
||
if (p) return p;
|
||
p = document.createElement('div');
|
||
p.className = 'mt-peek';
|
||
p.id = 'mt-peek';
|
||
p.setAttribute('aria-hidden', 'true');
|
||
p.innerHTML = '<div class="mt-peek-handle" id="mt-peek-handle"></div>' +
|
||
'<div class="mt-peek-head">' +
|
||
'<button class="mt-peek-btn" id="mt-peek-close" title="Close" ' +
|
||
'aria-label="Fermer">' + PEEK_SVG_CLOSE + '</button>' +
|
||
'<div class="mt-peek-title" id="mt-peek-title"></div>' +
|
||
'<button class="mt-peek-btn" id="mt-peek-full" title="Open full page" ' +
|
||
'aria-label="Ouvrir dans une page entière">' + PEEK_SVG_FULL + '</button>' +
|
||
'</div>' +
|
||
'<div class="mt-peek-body" id="mt-peek-body">' +
|
||
'<div class="mt-peek-loading" id="mt-peek-loading">Chargement…</div>' +
|
||
'<iframe id="mt-peek-iframe" title="Contenu de la tâche" ' +
|
||
'style="display:none"></iframe></div>';
|
||
document.body.appendChild(p);
|
||
|
||
document.getElementById('mt-peek-close').addEventListener('click', closePeek);
|
||
document.getElementById('mt-peek-full').addEventListener('click', function () {
|
||
var f = document.getElementById('mt-peek-iframe');
|
||
if (f && f.dataset.page) window.location.href = '/pages/' + f.dataset.page;
|
||
});
|
||
wirePeekResize(p);
|
||
return p;
|
||
}
|
||
|
||
function closePeek() {
|
||
var p = document.getElementById('mt-peek');
|
||
if (!p) return;
|
||
p.classList.remove('open');
|
||
p.setAttribute('aria-hidden', 'true');
|
||
var f = document.getElementById('mt-peek-iframe');
|
||
// On décharge l'iframe : elle garde sinon en mémoire tout l'éditeur
|
||
// d'un document que l'on vient de quitter.
|
||
if (f) { f.src = ''; delete f.dataset.page; f.style.display = 'none'; }
|
||
var l = document.getElementById('mt-peek-loading');
|
||
if (l) l.style.display = '';
|
||
}
|
||
|
||
function wirePeekResize(panel) {
|
||
// Helper partagé (app.js) : pointer capture, borne 300–90vw, clic =
|
||
// fermer, largeur persistée (localStorage fd_peek_width) — comme Library.
|
||
window.fdWirePeekResize(panel, document.getElementById('mt-peek-handle'), closePeek);
|
||
}
|
||
|
||
function openRowContent(rowId, title) {
|
||
var panel = ensurePeek();
|
||
document.getElementById('mt-peek-title').textContent = title || '';
|
||
var loading = document.getElementById('mt-peek-loading');
|
||
var frame = document.getElementById('mt-peek-iframe');
|
||
if (loading) loading.style.display = '';
|
||
if (frame) { frame.style.display = 'none'; frame.src = ''; delete frame.dataset.page; }
|
||
panel.classList.add('open');
|
||
panel.setAttribute('aria-hidden', 'false');
|
||
|
||
api('/db/pages/' + rowId + '/open/api').then(function (d) {
|
||
var pid = d && d.page_id;
|
||
if (!pid) throw new Error('page de contenu introuvable');
|
||
var f = document.getElementById('mt-peek-iframe');
|
||
if (!f || !document.getElementById('mt-peek').classList.contains('open')) return;
|
||
if (loading) loading.style.display = 'none';
|
||
f.dataset.page = pid;
|
||
f.style.display = '';
|
||
// `about:blank` d'abord : ouvrir une autre tâche doit REcharger
|
||
// l'éditeur, sinon l'URL identique laisse le document précédent
|
||
// affiché. `?embed=1` = le document en édition seule, sans
|
||
// sidebar ni barre de navigation (le cadre du panneau suffit).
|
||
f.src = 'about:blank';
|
||
f.src = '/pages/' + pid + '?embed=1';
|
||
}).catch(function (e) {
|
||
if (loading) {
|
||
loading.textContent = 'Impossible d’ouvrir le contenu : ' + e.message;
|
||
loading.style.display = '';
|
||
}
|
||
});
|
||
}
|
||
|
||
function syncUrl() {
|
||
// `replaceState` peut être refusé (iframe sandbox, `file://`,
|
||
// navigation cross-origin) : l'URL n'est qu'un confort, jamais
|
||
// l'interface — on ne laisse jamais une exception casser l'action.
|
||
try {
|
||
history.replaceState(null, '', '/my-tasks?' + qs());
|
||
} catch (e) { /* ignoré */ }
|
||
}
|
||
|
||
function qs() {
|
||
var p = new URLSearchParams();
|
||
p.set('view', state.view);
|
||
p.set('due', state.due);
|
||
p.set('sort', state.sort);
|
||
if (state.hide_done) p.set('hide_done', 'true');
|
||
return p.toString();
|
||
}
|
||
|
||
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;
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ── Données ───────────────────────────────────────────────────── */
|
||
|
||
function load() {
|
||
return api('/my-tasks/api?' + qs()).then(function (d) {
|
||
state.tasks = d.tasks || [];
|
||
state.sources = d.sources || [];
|
||
render();
|
||
}).catch(function (e) {
|
||
app.innerHTML = '<div class="my-tasks-error">' + esc(e.message) + '</div>';
|
||
});
|
||
}
|
||
|
||
function configuredSources() {
|
||
return state.sources.filter(function (s) { return s.configured; });
|
||
}
|
||
|
||
/* ── Barre d'outils ────────────────────────────────────────────── */
|
||
|
||
function toolbar() {
|
||
var h = '<div class="mt-toolbar">';
|
||
h += '<div class="mt-toolbar-left">';
|
||
h += viewTabs();
|
||
h += '</div><div class="mt-toolbar-right">';
|
||
h += select('mt-due-filter', 'due', [
|
||
['all', 'Toutes les échéances'],
|
||
['today', 'Échéance aujourd\'hui'],
|
||
['overdue', 'En retard + aujourd\'hui'],
|
||
['week', 'Cette semaine']
|
||
]);
|
||
h += select('mt-sort', 'sort', [
|
||
['due', 'Tri : échéance'],
|
||
['status', 'Tri : statut'],
|
||
['source', 'Tri : source'],
|
||
['created', 'Tri : création']
|
||
]);
|
||
h += '<label class="mt-toggle"><input type="checkbox" id="mt-hide-done"' +
|
||
(state.hide_done ? ' checked' : '') + '> Masquer terminées</label>';
|
||
h += '<button class="mt-new-btn" id="mt-new-btn">+ Nouvelle tâche</button>';
|
||
h += '</div></div>';
|
||
h += sourcesBar();
|
||
return h;
|
||
}
|
||
|
||
function viewTabs() {
|
||
var labels = { table: 'Tableau', board: 'Kanban', calendar: 'Calendrier' };
|
||
var h = '<div class="mt-view-tabs">';
|
||
Object.keys(labels).forEach(function (k) {
|
||
// `data-no-nav` : le changement de vue est traité ici, en JS.
|
||
// Sans lui, l'intercepteur de liens de `app.js` rejoue un
|
||
// chargement complet de la page et remplace le DOM rendu.
|
||
// Le `href` reste présent : le lien reste partageable et
|
||
// ouvrable dans un onglet.
|
||
h += '<a class="mt-view-tab' + (state.view === k ? ' active' : '') +
|
||
'" data-no-nav href="/my-tasks?' + withView(k) + '">' +
|
||
labels[k] + '</a>';
|
||
});
|
||
return h + '</div>';
|
||
}
|
||
|
||
function withView(v) {
|
||
var p = new URLSearchParams();
|
||
p.set('view', v);
|
||
p.set('due', state.due);
|
||
p.set('sort', state.sort);
|
||
if (state.hide_done) p.set('hide_done', 'true');
|
||
return p.toString();
|
||
}
|
||
|
||
function select(cls, key, opts) {
|
||
var s = '<select class="mt-select ' + cls + '" data-key="' + key + '">';
|
||
opts.forEach(function (o) {
|
||
s += '<option value="' + esc(o[0]) + '"' +
|
||
(state[key] === o[0] ? ' selected' : '') + '>' + esc(o[1]) + '</option>';
|
||
});
|
||
return s + '</select>';
|
||
}
|
||
|
||
function sourcesBar() {
|
||
var n = state.sources.length;
|
||
var ok = configuredSources().length;
|
||
var pend = state.sources.filter(function (s) { return !s.configured; });
|
||
var h = '<div class="mt-sources">';
|
||
h += '<span class="mt-sources-label">Sources : <b>' + ok + '/' + state.maxSources +
|
||
'</b> base(s) connectée(s)</span>';
|
||
state.sources.forEach(function (s) {
|
||
h += '<a class="mt-source-chip' + (s.configured ? '' : ' pending') +
|
||
'" href="/db/' + s.id + '" title="' + esc(s.workspace_name) + '">' +
|
||
esc(s.icon || '📋') + ' ' + esc(s.name) + '</a>';
|
||
// Une base connectée au mapping incomplet se corrige d'ici.
|
||
if (!s.configured) {
|
||
h += '<button class="mt-source-fix" data-fix-source="' + s.id +
|
||
'" title="Relier les colonnes manquantes : ' +
|
||
esc((s.missing_roles || []).join(', ')) + '">Configurer</button>';
|
||
}
|
||
});
|
||
if (pend.length) {
|
||
h += '<span class="mt-sources-warn" title="Ces bases n’émettent pas de tâche ' +
|
||
'tant que les 3 colonnes ne sont pas liées">⚠ ' + pend.length +
|
||
' base(s) à configurer</span>';
|
||
}
|
||
if (n >= state.maxSources) {
|
||
h += '<span class="mt-sources-warn">Limite de ' + state.maxSources +
|
||
' bases atteinte</span>';
|
||
}
|
||
return h + '</div>';
|
||
}
|
||
|
||
/* ── Vue tableau ──────────────────────────────────────────────────────
|
||
* Lignes en `div`, comme Library : c'est ce qui permet de n'afficher le
|
||
* bouton « Open » qu'au survol de la ligne (`.mt-hover-only`).
|
||
*/
|
||
|
||
// Bouton « Open » : SVG et libellé identiques à `.btn-open` de Library,
|
||
// pour que les deux listes se ressemblent exactement.
|
||
function openButton(t) {
|
||
return '<button class="mt-open-btn mt-hover-only" data-open-row="' + t.id +
|
||
'" data-open-title="' + esc(t.title) + '" ' +
|
||
'title="Open in side peek" aria-label="Ouvrir dans le panneau latéral">' +
|
||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
|
||
'<rect x="3" y="3" width="18" height="18" rx="2"/>' +
|
||
'<line x1="15" y1="3" x2="15" y2="21"/></svg> Open</button>';
|
||
}
|
||
|
||
function viewTable() {
|
||
if (!state.tasks.length) return emptyState();
|
||
var h = '<div class="mt-table">' +
|
||
'<div class="mt-thead">' +
|
||
'<div class="mt-th mt-cell-name">Tâche</div>' +
|
||
'<div class="mt-th mt-cell-status">Statut</div>' +
|
||
'<div class="mt-th mt-cell-due">Échéance</div>' +
|
||
'<div class="mt-th mt-cell-src">Source</div>' +
|
||
'</div>';
|
||
state.tasks.forEach(function (t) {
|
||
h += '<div class="mt-row" data-id="' + t.id + '">';
|
||
h += '<div class="mt-cell mt-cell-name">' +
|
||
'<span class="mt-icon">' + esc(rowIcon(t.icon)) + '</span>' +
|
||
'<span class="mt-title-text" data-edit="title" title="Cliquer pour renommer">' +
|
||
esc(t.title) + '</span>' + openButton(t) + '</div>';
|
||
h += '<div class="mt-cell mt-cell-status">' + statusCell(t) + '</div>';
|
||
h += '<div class="mt-cell mt-cell-due">' + dueCell(t) + '</div>';
|
||
h += '<div class="mt-cell mt-cell-src"><a class="mt-chip" href="/db/' +
|
||
t.collection_id + '" title="' + esc(t.workspace_name) +
|
||
' · ouvrir la base">' + esc(t.collection_icon) + ' ' +
|
||
esc(t.collection_name) + '</a></div>';
|
||
h += '</div>';
|
||
});
|
||
return h + '</div>';
|
||
}
|
||
|
||
function statusCell(t) {
|
||
if (!t.status_options || !t.status_options.length) {
|
||
return '<span class="mt-static">' + esc(t.status || '—') + '</span>';
|
||
}
|
||
return '<select class="mt-status-select" data-id="' + t.id +
|
||
'" data-field="status">' + t.status_options.map(function (o) {
|
||
return '<option value="' + esc(o) + '"' +
|
||
(o === t.status ? ' selected' : '') + '>' + esc(o) + '</option>';
|
||
}).join('') + '</select>';
|
||
}
|
||
|
||
function dueCell(t) {
|
||
var cls = t.due_state ? ' mt-due-' + t.due_state : '';
|
||
return '<input type="date" class="mt-due-input' + cls + '" data-id="' + t.id +
|
||
'" data-field="due" value="' + esc(t.due || '') + '">';
|
||
}
|
||
|
||
/* ── Vue kanban ────────────────────────────────────────────────── */
|
||
|
||
function viewBoard() {
|
||
var cols = [], seen = {};
|
||
state.tasks.forEach(function (t) {
|
||
(t.status_options || []).forEach(function (o) {
|
||
if (!seen[o]) { seen[o] = 1; cols.push(o); }
|
||
});
|
||
if (!t.status || seen[t.status] === undefined) {
|
||
if (t.status && !seen[t.status]) { seen[t.status] = 1; cols.push(t.status); }
|
||
}
|
||
});
|
||
if (!cols.length) cols = ['À faire'];
|
||
|
||
var h = '<div class="mt-board">';
|
||
cols.forEach(function (col) {
|
||
var items = state.tasks.filter(function (t) { return (t.status || '') === col; });
|
||
h += '<div class="mt-col" data-status="' + esc(col) + '">';
|
||
h += '<div class="mt-col-head">' + esc(col) + ' <span class="mt-count">' +
|
||
items.length + '</span></div>';
|
||
h += '<div class="mt-col-body">';
|
||
items.forEach(function (t) {
|
||
h += '<div class="mt-card" draggable="true" data-id="' + t.id + '">' +
|
||
'<span class="mt-icon">' + esc(rowIcon(t.icon)) + '</span>' +
|
||
'<span class="mt-card-title">' + esc(t.title) + '</span>' +
|
||
openButton(t) +
|
||
(t.due ? '<span class="mt-card-due mt-due-' + esc(t.due_state) + '">📅 ' +
|
||
esc(t.due) + '</span>' : '') +
|
||
'<span class="mt-card-src">' + esc(t.collection_icon) + ' ' +
|
||
esc(t.collection_name) + '</span></div>';
|
||
});
|
||
h += '</div></div>';
|
||
});
|
||
return h + '</div>';
|
||
}
|
||
|
||
/* ── Vue calendrier (5 vues) ─────────────────────────────────────── */
|
||
|
||
var CAL_VIEWS = [
|
||
['month', 'Mois'], ['year', 'Année'], ['week', 'Semaine'],
|
||
['day', 'Jour'], ['planning', 'Planning']
|
||
];
|
||
var OPEN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
|
||
'stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/>' +
|
||
'<line x1="15" y1="3" x2="15" y2="21"/></svg>';
|
||
|
||
function calEnsure() {
|
||
if (!state.month) {
|
||
var n = new Date();
|
||
state.month = { y: n.getFullYear(), m: n.getMonth() };
|
||
}
|
||
if (!state.calDay) {
|
||
var t = new Date();
|
||
state.calDay = t.getFullYear() + '-' +
|
||
('0' + (t.getMonth() + 1)).slice(-2) + '-' +
|
||
('0' + t.getDate()).slice(-2);
|
||
}
|
||
}
|
||
|
||
function isoDate(d) {
|
||
return d.getFullYear() + '-' +
|
||
('0' + (d.getMonth() + 1)).slice(-2) + '-' +
|
||
('0' + d.getDate()).slice(-2);
|
||
}
|
||
|
||
function calDate(iso) {
|
||
var p = String(iso || '').split('-');
|
||
return new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));
|
||
}
|
||
|
||
// Tâches groupées par jour d'échéance (celles sans échéance sont ignorées).
|
||
function tasksByDay() {
|
||
var byDay = {};
|
||
state.tasks.forEach(function (t) {
|
||
if (!t.due) return;
|
||
(byDay[t.due] = byDay[t.due] || []).push(t);
|
||
});
|
||
return byDay;
|
||
}
|
||
|
||
// Décalage du calendrier, la granularité dépendant de la vue courante.
|
||
function calShift(delta) {
|
||
calEnsure();
|
||
var v = state.calView;
|
||
if (v === 'year') {
|
||
state.month = { y: state.month.y + delta, m: state.month.m };
|
||
} else if (v === 'week') {
|
||
var w = calDate(state.calDay);
|
||
w.setDate(w.getDate() + delta * 7);
|
||
state.calDay = isoDate(w);
|
||
} else if (v === 'day') {
|
||
var d = calDate(state.calDay);
|
||
d.setDate(d.getDate() + delta);
|
||
state.calDay = isoDate(d);
|
||
state.month = { y: d.getFullYear(), m: d.getMonth() };
|
||
} else {
|
||
var m = state.month.m + delta;
|
||
state.month = {
|
||
m: ((m % 12) + 12) % 12,
|
||
y: state.month.y + Math.floor(m / 12)
|
||
};
|
||
}
|
||
render();
|
||
}
|
||
|
||
function calTitle() {
|
||
calEnsure();
|
||
var y = state.month.y, m = state.month.m;
|
||
if (state.calView === 'year') return String(y);
|
||
if (state.calView === 'planning') return 'Planning — ' + MONTHS[m] + ' ' + y;
|
||
if (state.calView === 'day') {
|
||
var d = calDate(state.calDay);
|
||
return DAYS[(d.getDay() + 6) % 7] + ' ' + d.getDate() + ' ' +
|
||
MONTHS[d.getMonth()] + ' ' + d.getFullYear();
|
||
}
|
||
if (state.calView === 'week') {
|
||
var s = calDate(state.calDay);
|
||
var lead = (s.getDay() + 6) % 7; // lundi = 0
|
||
s.setDate(s.getDate() - lead);
|
||
var e = new Date(s.getFullYear(), s.getMonth(), s.getDate() + 6);
|
||
return s.getDate() + ' – ' + e.getDate() + ' ' + MONTHS[e.getMonth()] +
|
||
' ' + e.getFullYear();
|
||
}
|
||
return MONTHS[m] + ' ' + y;
|
||
}
|
||
|
||
function calTabs() {
|
||
var h = '<div class="mt-cal-tabs">';
|
||
CAL_VIEWS.forEach(function (v) {
|
||
h += '<button class="mt-cal-tab' + (state.calView === v[0] ? ' active' : '') +
|
||
'" data-cal-view="' + v[0] + '">' + v[1] + '</button>';
|
||
});
|
||
return h + '</div>';
|
||
}
|
||
|
||
// Événement de calendrier : titre + bouton d'ouverture au survol.
|
||
function calEvHtml(t, draggable) {
|
||
return '<div class="mt-cal-ev' + (draggable ? ' drag' : '') + '"' +
|
||
(draggable ? ' draggable="true" data-id="' + t.id + '"' : '') +
|
||
' title="' + esc(t.collection_name) + ' · ' + esc(t.status || '') +
|
||
' · ' + esc(t.due || '') + '">' +
|
||
'<span class="mt-cal-ev-text">' + esc(t.title) + '</span>' +
|
||
'<button class="mt-cal-open mt-hover-only" data-open-row="' + t.id +
|
||
'" data-open-title="' + esc(t.title) + '" title="Open in side peek" ' +
|
||
'aria-label="Ouvrir dans le panneau latéral">' + OPEN_SVG + '</button>' +
|
||
'</div>';
|
||
}
|
||
|
||
function viewCalendar() {
|
||
calEnsure();
|
||
var h = '<div class="mt-cal">' +
|
||
'<div class="mt-cal-head">' +
|
||
'<button class="mt-cal-nav" data-nav="-1" aria-label="Précédent">‹</button>' +
|
||
'<span class="mt-cal-title">' + esc(calTitle()) + '</span>' +
|
||
'<button class="mt-cal-nav" data-nav="1" aria-label="Suivant">›</button>' +
|
||
'</div>' + calTabs() + '<div class="mt-cal-body">';
|
||
if (state.calView === 'year') h += calYear();
|
||
else if (state.calView === 'week') h += calWeek();
|
||
else if (state.calView === 'day') h += calDay();
|
||
else if (state.calView === 'planning') h += calPlanning();
|
||
else h += calMonth();
|
||
return h + '</div></div>';
|
||
}
|
||
|
||
/* Mois : grille 7 × 6, glisser-déposer pour changer l'échéance. */
|
||
function calMonth() {
|
||
calEnsure();
|
||
var y = state.month.y, m = state.month.m;
|
||
var byDay = tasksByDay();
|
||
var first = new Date(y, m, 1);
|
||
var lead = (first.getDay() + 6) % 7; // lundi = 0
|
||
var start = new Date(y, m, 1 - lead);
|
||
var todayIso = isoDate(new Date());
|
||
|
||
var h = '<div class="mt-cal-grid">';
|
||
DAYS.forEach(function (d) { h += '<div class="mt-cal-dow">' + d + '</div>'; });
|
||
for (var i = 0; i < 42; i++) {
|
||
var d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
|
||
var key = isoDate(d);
|
||
var evs = byDay[key] || [];
|
||
h += '<div class="mt-cal-cell' + (d.getMonth() !== m ? ' out' : '') +
|
||
(key === todayIso ? ' today' : '') + '" data-day="' + key + '">';
|
||
h += '<div class="mt-cal-day">' + d.getDate() + '</div>';
|
||
evs.forEach(function (t) { h += calEvHtml(t, true); });
|
||
h += '</div>';
|
||
}
|
||
return h + '</div>';
|
||
}
|
||
|
||
/* Année : 12 mini-mois ; un point signale un jour chargé. */
|
||
function calYear() {
|
||
calEnsure();
|
||
var byDay = tasksByDay();
|
||
var todayIso = isoDate(new Date());
|
||
var h = '<div class="mt-cal-year">';
|
||
for (var mo = 0; mo < 12; mo++) {
|
||
var y = state.month.y;
|
||
var first = new Date(y, mo, 1);
|
||
var lead = (first.getDay() + 6) % 7;
|
||
var start = new Date(y, mo, 1 - lead);
|
||
var dim = new Date(y, mo + 1, 0).getDate();
|
||
h += '<div class="mt-cal-mini">';
|
||
h += '<div class="mt-cal-mini-title">' + MONTHS[mo] + '</div>';
|
||
h += '<div class="mt-cal-mini-grid">';
|
||
DAYS.forEach(function (d) {
|
||
h += '<span class="mt-cal-mini-dow">' + d.charAt(0) + '</span>';
|
||
});
|
||
for (var i = 0; i < lead; i++) h += '<span class="mt-cal-mini-day out"></span>';
|
||
for (var dd = 1; dd <= dim; dd++) {
|
||
var key = y + '-' + ('0' + (mo + 1)).slice(-2) + '-' +
|
||
('0' + dd).slice(-2);
|
||
var n = (byDay[key] || []).length;
|
||
h += '<button class="mt-cal-mini-day' + (key === todayIso ? ' today' : '') +
|
||
(n ? ' busy' : '') + '" data-cal-day="' + key + '" ' +
|
||
'title="' + dd + ' ' + MONTHS[mo] + (n ? ' — ' + n + ' tâche(s)' : '') + '">' +
|
||
dd + '</button>';
|
||
}
|
||
h += '</div></div>';
|
||
}
|
||
return h + '</div>';
|
||
}
|
||
|
||
/* Semaine : 7 colonnes, une par jour. */
|
||
function calWeek() {
|
||
calEnsure();
|
||
var byDay = tasksByDay();
|
||
var s = calDate(state.calDay);
|
||
var lead = (s.getDay() + 6) % 7; // lundi = 0
|
||
s.setDate(s.getDate() - lead);
|
||
var todayIso = isoDate(new Date());
|
||
var h = '<div class="mt-cal-week">';
|
||
for (var i = 0; i < 7; i++) {
|
||
var d = new Date(s.getFullYear(), s.getMonth(), s.getDate() + i);
|
||
var key = isoDate(d);
|
||
var evs = byDay[key] || [];
|
||
h += '<div class="mt-cal-wcol' + (key === todayIso ? ' today' : '') + '">';
|
||
h += '<div class="mt-cal-wcol-head">' + DAYS[i] + ' ' + d.getDate() + '</div>';
|
||
h += '<div class="mt-cal-wcol-body">';
|
||
if (!evs.length) h += '<div class="mt-cal-wempty">—</div>';
|
||
evs.forEach(function (t) { h += calEvHtml(t, false); });
|
||
h += '</div></div>';
|
||
}
|
||
return h + '</div>';
|
||
}
|
||
|
||
/* Jour : la liste complète des tâches du jour sélectionné. */
|
||
function calDay() {
|
||
calEnsure();
|
||
var byDay = tasksByDay();
|
||
var key = state.calDay;
|
||
var evs = byDay[key] || [];
|
||
var h = '<div class="mt-cal-dayview">';
|
||
h += '<div class="mt-cal-dayview-head">' + esc(calTitle()) +
|
||
' · <b>' + evs.length + '</b> tâche(s)</div>';
|
||
if (!evs.length) {
|
||
h += '<div class="mt-cal-dayview-empty">Aucune tâche pour ce jour.</div>';
|
||
return h + '</div>';
|
||
}
|
||
h += '<div class="mt-cal-dayview-list">';
|
||
evs.forEach(function (t) {
|
||
h += '<div class="mt-cal-task" data-id="' + t.id + '">' +
|
||
'<span class="mt-icon">' + esc(rowIcon(t.icon)) + '</span>' +
|
||
'<span class="mt-cal-task-title">' + esc(t.title) + '</span>' +
|
||
'<span class="mt-cal-task-src">' + esc(t.collection_icon) + ' ' +
|
||
esc(t.collection_name) + '</span>' +
|
||
'<span class="mt-cal-task-status">' + esc(t.status || '—') + '</span>' +
|
||
'<button class="mt-open-btn" data-open-row="' + t.id +
|
||
'" data-open-title="' + esc(t.title) + '" title="Open in side peek" ' +
|
||
'aria-label="Ouvrir dans le panneau latéral">' + OPEN_SVG + ' Open</button>' +
|
||
'</div>';
|
||
});
|
||
return h + '</div></div>';
|
||
}
|
||
|
||
/* Planning : colonnes = statuts, lignes = jours du mois (tâches placées
|
||
à leur date d'échéance). C'est la vue « planning » de Notion. */
|
||
function calPlanning() {
|
||
calEnsure();
|
||
var byDay = tasksByDay();
|
||
var y = state.month.y, m = state.month.m;
|
||
var statuses = [], seen = {};
|
||
state.tasks.forEach(function (t) {
|
||
if (!t.due) return;
|
||
var s = t.status || 'Sans statut';
|
||
if (!seen[s]) { seen[s] = 1; statuses.push(s); }
|
||
});
|
||
if (!statuses.length) statuses = ['Sans statut'];
|
||
var dim = new Date(y, m + 1, 0).getDate();
|
||
var todayIso = isoDate(new Date());
|
||
|
||
var h = '<div class="mt-cal-plan">';
|
||
h += '<div class="mt-cal-plan-row head"><div class="mt-cal-plan-gutter"></div>';
|
||
statuses.forEach(function (s) {
|
||
h += '<div class="mt-cal-plan-col-title">' + esc(s) + '</div>';
|
||
});
|
||
h += '</div>';
|
||
for (var d = 1; d <= dim; d++) {
|
||
var key = y + '-' + ('0' + (m + 1)).slice(-2) + '-' + ('0' + d).slice(-2);
|
||
var evs = byDay[key] || [];
|
||
h += '<div class="mt-cal-plan-row' + (key === todayIso ? ' today' : '') + '">' +
|
||
'<div class="mt-cal-plan-gutter">' + d + '</div>';
|
||
statuses.forEach(function (s) {
|
||
h += '<div class="mt-cal-plan-cell">';
|
||
evs.filter(function (t) {
|
||
return (t.status || 'Sans statut') === s;
|
||
}).forEach(function (t) { h += calEvHtml(t, false); });
|
||
h += '</div>';
|
||
});
|
||
h += '</div>';
|
||
}
|
||
return h + '</div>';
|
||
}
|
||
|
||
/* ── États vides ───────────────────────────────────────────────── */
|
||
|
||
function emptyState() {
|
||
var title, msg, hint, act;
|
||
var connected = configuredSources().length;
|
||
if (!state.sources.length) {
|
||
title = 'Aucune base connectée';
|
||
msg = "Aucune base de tâches n'alimente ce tableau de bord.";
|
||
hint = 'Ouvrez une base, puis cliquez sur « Convertir en base de tâches » ' +
|
||
'dans sa barre (à gauche du bouton New) et reliez les colonnes ' +
|
||
'Assigné à, Statut et Échéance.';
|
||
act = '<a class="mt-empty-btn" href="/db">Ouvrir mes bases</a>';
|
||
} else if (!connected) {
|
||
title = 'Mapping incomplet';
|
||
msg = 'Bases connectées mais aucune configurée.';
|
||
hint = 'Il manque le mapping Assigné à / Statut / Échéance. Ouvrez la ' +
|
||
'base et cliquez sur « Configurer » dans sa barre.';
|
||
act = '<a class="mt-empty-btn" href="/db">Ouvrir mes bases</a>';
|
||
} else if (state.due !== 'all' || state.hide_done || state.sort !== 'due') {
|
||
title = 'Aucun résultat';
|
||
msg = 'Aucune tâche ne correspond aux filtres actifs.';
|
||
hint = '';
|
||
act = '<a class="mt-empty-btn" href="/my-tasks">Réinitialiser les filtres</a>';
|
||
} else {
|
||
title = 'Rien à faire';
|
||
msg = "Aucune tâche ne vous est assignée pour le moment.";
|
||
hint = 'Ajoutez-en une, ou branchesz une autre base pour voir plus large.';
|
||
act = '<button class="mt-empty-btn" id="mt-new-btn">Créer une tâche</button>';
|
||
}
|
||
// Même gabarit centré que l'état vide rendu par le serveur
|
||
// (`_render_empty`), pour que les deux se ressemblent.
|
||
return '<div class="mt-empty"><div class="mt-empty-icon">📋</div>' +
|
||
'<h2>' + esc(title) + '</h2><p>' + esc(msg) + '</p>' +
|
||
(hint ? '<p class="mt-empty-hint">' + esc(hint) + '</p>' : '') +
|
||
act + '</div>';
|
||
}
|
||
|
||
/* ── Modale « nouvelle tâche » (multi-destinations) ────────────── */
|
||
|
||
// Ouvre la modale de conversion pour une base donnée (les bases
|
||
// connectées mais mal configurées ne peuvent pas recevoir de tâche).
|
||
function openConvertModal(collectionId) {
|
||
if (collectionId && window.FlowDeckTaskDb) {
|
||
window.FlowDeckTaskDb.open(Number(collectionId));
|
||
}
|
||
}
|
||
|
||
function openNewModal() {
|
||
var srcs = configuredSources();
|
||
if (!srcs.length) {
|
||
alert("Aucune base de tâches connectée.\n\nConvertissez d'abord une base : " +
|
||
"ouvrez-la, puis cliquez sur « Convertir en base de tâches » " +
|
||
"dans la barre de la base, à gauche du bouton New.");
|
||
return;
|
||
}
|
||
var groups = {};
|
||
srcs.forEach(function (s) {
|
||
(groups[s.workspace_name || 'Autres'] = groups[s.workspace_name || 'Autres'] || [])
|
||
.push(s);
|
||
});
|
||
var h = '<div class="mt-modal-overlay" id="mt-modal-overlay"><div class="mt-modal">';
|
||
h += '<div class="mt-modal-head"><span>Nouvelle tâche</span>' +
|
||
'<button class="mt-modal-close" id="mt-modal-close">✕</button></div>';
|
||
h += '<div class="mt-modal-body">';
|
||
h += '<label class="mt-field">Titre<input id="mt-new-title" type="text" ' +
|
||
'placeholder="Ex. Envoyer la maquette"></label>';
|
||
h += '<label class="mt-field">Échéance<input id="mt-new-due" type="date"></label>';
|
||
Object.keys(groups).forEach(function (ws) {
|
||
h += '<div class="mt-modal-group">' + esc(ws) + '</div>';
|
||
groups[ws].forEach(function (s) {
|
||
h += '<label class="mt-dest"><input type="radio" name="mt-dest" value="' +
|
||
s.id + '"><span class="mt-icon">' + esc(s.icon || '📋') + '</span>' +
|
||
esc(s.name) + '</label>';
|
||
});
|
||
});
|
||
h += '</div><div class="mt-modal-foot">' +
|
||
'<button class="mt-btn-cancel" id="mt-new-cancel">Annuler</button>' +
|
||
'<button class="mt-btn-primary" id="mt-new-create">Créer</button></div>';
|
||
h += '</div></div>';
|
||
document.body.insertAdjacentHTML('beforeend', h);
|
||
|
||
var first = document.querySelector('input[name="mt-dest"]');
|
||
if (first) first.checked = true;
|
||
document.getElementById('mt-new-title').focus();
|
||
|
||
var close = function () {
|
||
var ov = document.getElementById('mt-modal-overlay');
|
||
if (ov) ov.remove();
|
||
};
|
||
// `Escape` ferme aussi la modale.
|
||
document.addEventListener('keydown', function onEsc(e) {
|
||
if (e.key !== 'Escape') return;
|
||
document.removeEventListener('keydown', onEsc);
|
||
close();
|
||
});
|
||
document.getElementById('mt-modal-close').onclick = close;
|
||
document.getElementById('mt-new-cancel').onclick = close;
|
||
document.getElementById('mt-modal-overlay').onclick = function (e) {
|
||
if (e.target === this) close();
|
||
};
|
||
document.getElementById('mt-new-create').onclick = function () {
|
||
var title = document.getElementById('mt-new-title').value.trim();
|
||
var due = document.getElementById('mt-new-due').value;
|
||
var dest = document.querySelector('input[name="mt-dest"]:checked');
|
||
if (!title) { alert('Donnez un titre à la tâche.'); return; }
|
||
if (!dest) { alert('Choisissez une base de destination.'); return; }
|
||
api('/my-tasks/api/task', {
|
||
method: 'POST',
|
||
body: { collection_id: Number(dest.value), title: title, due: due || null }
|
||
}).then(function () { close(); load(); })
|
||
.catch(function (e) { close(); alert(e.message); });
|
||
};
|
||
}
|
||
|
||
/* ── Écriture en direct ────────────────────────────────────────── */
|
||
|
||
function patch(id, field, value) {
|
||
return api('/my-tasks/api/task/' + id, { method: 'PUT', body: { field: field, value: value } })
|
||
.then(function (d) {
|
||
if (d.left_mytasks) {
|
||
state.tasks = state.tasks.filter(function (t) { return t.id !== id; });
|
||
render();
|
||
} else if (d.task) {
|
||
state.tasks = state.tasks.map(function (t) {
|
||
return t.id === d.task.id ? d.task : t;
|
||
});
|
||
render();
|
||
}
|
||
})
|
||
.catch(function (e) { alert(e.message); load(); });
|
||
}
|
||
|
||
function startTitleEdit(span) {
|
||
if (span.querySelector('input')) return;
|
||
var tr = span.closest('.mt-row');
|
||
var id = Number(tr.dataset.id);
|
||
var old = span.textContent;
|
||
var input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.className = 'mt-title-input';
|
||
input.value = old;
|
||
span.textContent = '';
|
||
span.appendChild(input);
|
||
input.focus();
|
||
input.select();
|
||
var done = false;
|
||
var commit = function () {
|
||
if (done) return;
|
||
done = true;
|
||
var v = input.value.trim();
|
||
if (v && v !== old) patch(id, 'title', v); else render();
|
||
};
|
||
input.onblur = commit;
|
||
input.onkeydown = function (e) {
|
||
if (e.key === 'Enter') { e.preventDefault(); commit(); }
|
||
if (e.key === 'Escape') { done = true; render(); }
|
||
};
|
||
}
|
||
|
||
/* ── Rendu + délégation d'événements ───────────────────────────── */
|
||
|
||
function render() {
|
||
var body = state.view === 'board' ? viewBoard()
|
||
: state.view === 'calendar' ? viewCalendar()
|
||
: viewTable();
|
||
app.innerHTML = '<div class="mt-head"><h1>📋 My Tasks</h1>' +
|
||
'<span class="mt-count-total">' + state.tasks.length + ' tâche(s)</span></div>' +
|
||
toolbar() + body;
|
||
wireDnD();
|
||
}
|
||
|
||
function wireDnD() {
|
||
var cards = app.querySelectorAll('[draggable="true"]');
|
||
cards.forEach(function (c) {
|
||
c.addEventListener('dragstart', function (e) {
|
||
e.dataTransfer.setData('text/plain', c.dataset.id);
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
});
|
||
});
|
||
app.querySelectorAll('.mt-col').forEach(function (col) {
|
||
col.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||
col.addEventListener('drop', function (e) {
|
||
e.preventDefault();
|
||
var id = Number(e.dataTransfer.getData('text/plain'));
|
||
if (id && col.dataset.status) patch(id, 'status', col.dataset.status);
|
||
});
|
||
});
|
||
// Calendrier : glisser une tâche sur un jour change son échéance.
|
||
app.querySelectorAll('.mt-cal-cell').forEach(function (cell) {
|
||
cell.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||
cell.addEventListener('drop', function (e) {
|
||
e.preventDefault();
|
||
var id = Number(e.dataTransfer.getData('text/plain'));
|
||
if (id && cell.dataset.day) patch(id, 'due', cell.dataset.day);
|
||
});
|
||
});
|
||
}
|
||
|
||
if (wire) app.addEventListener('click', function (e) {
|
||
var t = e.target;
|
||
|
||
if (t.id === 'mt-new-btn') { openNewModal(); return; }
|
||
var opener = t.closest ? t.closest('[data-open-row]') : null;
|
||
if (opener) {
|
||
// `closest` : dans le calendrier le bouton ne contient que son
|
||
// SVG — un clic sur l'icone donnait `e.target` = <svg> sans
|
||
// dataset, et le bouton ne faisait rien.
|
||
e.preventDefault();
|
||
// Le titre vient de l'attribut : il est disponible aussi bien dans
|
||
// le tableau que dans le calendrier ou la vue Jour.
|
||
var label = opener.dataset.openTitle;
|
||
if (!label) {
|
||
var row = opener.closest('.mt-row');
|
||
var cell = row && row.querySelector('.mt-title-text');
|
||
label = cell ? cell.textContent : '';
|
||
}
|
||
openRowContent(Number(opener.dataset.openRow), label);
|
||
return;
|
||
}
|
||
if (t.dataset && t.dataset.fixSource) {
|
||
e.preventDefault();
|
||
openConvertModal(t.dataset.fixSource);
|
||
return;
|
||
}
|
||
if (t.classList.contains('mt-cal-nav')) {
|
||
calShift(Number(t.dataset.nav));
|
||
return;
|
||
}
|
||
if (t.dataset && t.dataset.calView) {
|
||
state.calView = t.dataset.calView;
|
||
render();
|
||
return;
|
||
}
|
||
if (t.dataset && t.dataset.calDay) {
|
||
// Un jour de la vue Année ouvre la vue Jour dessus.
|
||
state.calDay = t.dataset.calDay;
|
||
var cd = calDate(state.calDay);
|
||
state.month = { y: cd.getFullYear(), m: cd.getMonth() };
|
||
state.calView = 'day';
|
||
render();
|
||
return;
|
||
}
|
||
if (t.dataset && t.dataset.edit === 'title') { startTitleEdit(t); return; }
|
||
|
||
var link = t.closest ? t.closest('a.mt-view-tab') : null;
|
||
if (link) {
|
||
e.preventDefault();
|
||
var p = new URLSearchParams(link.getAttribute('href').split('?')[1] || '');
|
||
state.view = p.get('view') || 'table';
|
||
syncUrl();
|
||
render();
|
||
}
|
||
});
|
||
|
||
if (wire) app.addEventListener('change', function (e) {
|
||
var t = e.target;
|
||
if (t.classList.contains('mt-select')) {
|
||
state[t.dataset.key] = t.value;
|
||
syncUrl();
|
||
load();
|
||
} else if (t.id === 'mt-hide-done') {
|
||
state.hide_done = t.checked;
|
||
syncUrl();
|
||
load();
|
||
} else if (t.classList.contains('mt-status-select')) {
|
||
patch(Number(t.dataset.id), 'status', t.value);
|
||
} else if (t.classList.contains('mt-due-input')) {
|
||
patch(Number(t.dataset.id), 'due', t.value);
|
||
}
|
||
});
|
||
|
||
load();
|
||
|
||
/* Une modale ou un panneau restés ouverts pendant une navigation
|
||
* masqueraient la page suivante : on referme au remontage. */
|
||
if (wire) {
|
||
var staleModal = document.getElementById('mt-modal-overlay');
|
||
if (staleModal) staleModal.remove();
|
||
closePeek();
|
||
document.addEventListener('keydown', function (e) {
|
||
// `Escape` ferme le panneau latéral, comme dans Library.
|
||
if (e.key === 'Escape') closePeek();
|
||
});
|
||
}
|
||
}
|
||
|
||
// htmx ré-exécute les <script> de la zone swappée : on remonte à chaque
|
||
// navigation interne. `popstate` couvre le bouton « retour », où le <script>
|
||
// n'est pas forcément rejoué.
|
||
document.addEventListener('htmx:afterSwap', flowDeckMyTasks);
|
||
window.addEventListener('popstate', flowDeckMyTasks);
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', flowDeckMyTasks);
|
||
} else {
|
||
flowDeckMyTasks();
|
||
}
|