if (!window.__fdDbTableScriptsLoaded) { window.__fdDbTableScriptsLoaded = true; const DB_CONFIG=(()=>{try{const el=document.getElementById('db-config');const v=el?JSON.parse(el.textContent):null;return v===undefined?null:v}catch{return null}})(); (function() { 'use strict'; // ── SVG Outline Icons ── function propTypeSvg(type) { const s = 14; switch(type) { case 'text': case 'title': return ''; case 'number': return ''; case 'select': return ''; case 'multi_select': return ''; case 'status': return ''; case 'date': return ''; case 'checkbox': return ''; case 'url': return ''; case 'email': return ''; case 'phone': return ''; case 'person': return ''; case 'files': return ''; case 'relation': case 'rollup': case 'formula': return ''; case 'created_time': case 'last_edited_time': return ''; case 'created_by': case 'last_edited_by': return ''; default: return ''; } } var AUTO_TYPES = ['created_time','created_by','last_edited_time','last_edited_by']; var READONLY_TYPES = AUTO_TYPES.concat(['rollup','formula']); var PALETTE = {gray:'#8b8b8b',brown:'#9f6b53',orange:'#d9730d',yellow:'#cb912f',green:'#448361',blue:'#337ea9',purple:'#9065b0',pink:'#c14c8a',red:'#d44c47'}; function escHtml(s) { return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function parseJSON(s, d) { try { return typeof s === 'string' ? JSON.parse(s) : (s || d); } catch { return d; } } function parseOpts(prop) { var o = parseJSON(prop.options_json, []); if (!Array.isArray(o)) o = []; return o.map(function(x){ return typeof x === 'string' ? {name:x,color:'gray'} : {name:x.name||'',color:x.color||'gray'}; }); } function optColor(prop, name) { var found = parseOpts(prop).filter(function(o){ return o.name === name; })[0]; return (found && PALETTE[found.color]) || '#8b8b8b'; } function toast(msg, kind) { if (typeof window.showToast === 'function') window.showToast(msg, kind); else if (kind === 'error') console.warn(msg); } function pvOf(cp) { return parseJSON(cp.property_values_json, {}) || {}; } function pageVal(cp, prop) { if (prop.prop_type === 'title') return cp.title || ''; var pv = pvOf(cp); var v = pv[String(prop.id)]; if (v === undefined) v = pv[prop.name]; return v; } function setPageVal(cp, prop, val) { var pv = pvOf(cp); pv[String(prop.id)] = val; cp.property_values_json = pv; if (prop.prop_type === 'title') cp.title = val; } // ── v5.8.0: recurrence / reminder meta keys ── function metaOf(cp, key) { var m = pvOf(cp)[key]; return (m && typeof m === 'object' && !Array.isArray(m)) ? m : null; } function setMeta(cp, key, propId, val) { var pv = pvOf(cp); var m = pv[key]; if (!m || typeof m !== 'object' || Array.isArray(m)) m = {}; if (val === null || val === undefined) delete m[String(propId)]; else m[String(propId)] = val; if (Object.keys(m).length) pv[key] = m; else delete pv[key]; cp.property_values_json = pv; } function recBadgeHtml(cp) { var rec = metaOf(cp, '__recurrence__'); var rem = metaOf(cp, '__reminder__'); var h = ''; if (rec) { Object.keys(rec).forEach(function(k){ if (rec[k] && rec[k].freq) h += '\u21bb'; }); } if (rem) { Object.keys(rem).forEach(function(k){ var r=rem[k]; if (r && r.unit && r.unit!=='none') h += '🔔'; }); } return h; } // ── Value display (type-aware) ── function initials(s) { s = (s||'?').trim(); return s ? s.charAt(0).toUpperCase() : '?'; } function avatarHtml(p, size) { size = size || 16; var bg = p.avatar_color || '#5b6b8c'; if (p.avatar_url) return ''; return ''+escHtml(initials(p.login||p.full_name))+''; } function displayValue(prop, val) { if (val === undefined || val === null || val === '') return '\u2014'; var t = prop.prop_type; if (t === 'checkbox') return ''+(val ? '✓' : '')+''; if (t === 'person' && Array.isArray(val)) { if (!val.length) return '\u2014'; return val.map(function(p){ return ''+avatarHtml(p)+escHtml(p.login||p.full_name||'')+''; }).join(''); } if ((t === 'select' || t === 'status')) { return ''+escHtml(val)+''; } if (t === 'multi_select' && Array.isArray(val)) { return val.map(function(v){ return ''+escHtml(v)+''; }).join('') || '\u2014'; } if (t === 'date') { return ''+escHtml(String(val).slice(0,10))+''; } if (t === 'created_time' || t === 'last_edited_time') { return ''+escHtml(String(val).slice(0,16).replace('T',' '))+''; } if (t === 'created_by' || t === 'last_edited_by') { return ''+avatarHtml(val)+escHtml(val.login||val.full_name||'')+''; } if (t === 'url' && val) return ''+escHtml(val)+''; if (t === 'email' && val) return ''+escHtml(val)+''; if (t === 'relation' && Array.isArray(val)) return ''+escHtml(val.join(', '))+''; return ''+escHtml(Array.isArray(val)?val.join(', '):String(val))+''; } function textValue(prop, val) { if (val === undefined || val === null) return ''; if (Array.isArray(val)) return val.map(function(v){ return (v && (v.login||v.full_name)) || v; }).join(', '); return String(val); } // ── DB Instance (one per container) ── function DBInstance(containerEl, collectionId, initialData) { var state = { collection: (initialData && initialData.collection) || {}, properties: (initialData && initialData.properties) || [], pages: (initialData && initialData.pages) || [], views: (initialData && initialData.views) || [], members: null, activeViewId: null, viewConfig: {}, viewType: 'table', editingCell: null, colWidths: {}, collapsedGroups: {}, calCursor: new Date(), zones: null }; function propsById() { var m = {}; state.properties.forEach(function(p){ m[String(p.id)] = p; }); return m; } function findProp(ref) { if (ref === undefined || ref === null || ref === '') return null; return state.properties.filter(function(p){ return String(p.id) === String(ref) || p.name === ref; })[0] || null; } function firstPropOfTypes(types) { return state.properties.filter(function(p){ return types.indexOf(p.prop_type) >= 0; })[0] || null; } function pageById(id) { return state.pages.filter(function(p){ return p.id === id; })[0]; } function groupProp() { return findProp(state.viewConfig.group_by) || firstPropOfTypes(['status','select']) || state.properties[0]; } function activeView() { return state.views.filter(function(v){ return v.id === state.activeViewId; })[0] || null; } function applyView(v) { if (!v) { state.activeViewId = null; state.viewType = 'table'; state.viewConfig = {}; return; } state.activeViewId = v.id; state.viewType = v.view_type || 'table'; state.viewConfig = parseJSON(v.config_json, {}) || {}; } function ensureView() { if (state.views.length) { applyView(state.views[0]); return Promise.resolve(); } return fetch('/db/'+collectionId+'/views/save-as', { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({name:'Table', view_type:'table', config:{}}) }).then(function(r){ return r.json(); }).then(function(d){ state.views = [d]; applyView(d); }).catch(function(){ applyView(null); }); } function persistConfig(patch, opts) { opts = opts || {}; Object.keys(patch).forEach(function(k){ state.viewConfig[k] = patch[k]; }); var v = activeView(); if (!v) { return fetch('/db/'+collectionId+'/views/save-as', { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({name: state.viewType.charAt(0).toUpperCase()+state.viewType.slice(1), view_type: state.viewType, config: state.viewConfig}) }).then(function(r){ return r.json(); }).then(function(d){ state.views.push(d); applyView(d); render(); }); } if (opts.name) v.name = opts.name; if (opts.viewType) { v.view_type = opts.viewType; state.viewType = opts.viewType; } v.config_json = JSON.stringify(state.viewConfig); if (!opts.silent) render(); return fetch('/db/views/'+v.id+'/config', { method:'PUT', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify(Object.assign({}, patch, opts.name?{name:opts.name}:{}, opts.viewType?{view_type:opts.viewType}:{})) }).catch(function(){}); } // ── Root render ── function render() { state.properties.forEach(function(p){ if (!state.colWidths[p.id]) state.colWidths[p.id] = 200; }); var h = renderViewBar(); if (state.viewType === 'board') h += renderBoard(); else if (state.viewType === 'calendar') h += renderCalendar(); else if (state.viewType === 'gallery') h += renderGallery(); else if (state.viewType === 'list') h += renderList(); else h += renderTable(); containerEl.innerHTML = h; bindEvents(); } var VIEW_ICONS = { table:'', board:'', calendar:'', gallery:'', list:'' }; function viewSvg(t) { return ''+(VIEW_ICONS[t]||VIEW_ICONS.table)+''; } function taskDbButton(collection) { // Le libellé suit l'état réel de la base : convertie, à // configurer, ou pas encore convertie. Un simple ✓ était // introuvable pour l'utilisateur. var isTask = !!(collection && collection.is_task); var mapped = !!(collection && collection.task_assignee_prop && collection.task_status_prop && collection.task_due_prop); var label = isTask ? (mapped ? '✓ Base de tâches' : '⚠ Configurer') : 'Convertir en base de tâches'; var title = isTask ? (mapped ? 'Cette base alimente My Tasks' : 'Base connectée mais mapping incomplet — reenregistrer') : 'Relier Assigné à / Statut / Échéance pour alimenter My Tasks'; return ''; } function renderViewBar() { var h = '
'; if (!state.views.length) { h += ''; } state.views.forEach(function(v){ var active = v.id === state.activeViewId; h += ''; }); h += ''; h += '
'; h += '
' + taskDbButton(state.collection) + '
'; h += '
'; return h; } function bindViewBar() { containerEl.querySelectorAll('[data-view-id]').forEach(function(b){ b.onclick = function(e){ if (e.target.closest('[data-view-delete]') || e.target.closest('[data-view-settings]')) return; var id = this.dataset.viewId; if (!id) return; var v = state.views.filter(function(x){ return String(x.id) === String(id); })[0]; if (v) { applyView(v); render(); } }; }); containerEl.querySelectorAll('[data-view-delete]').forEach(function(b){ b.onclick = function(e){ e.stopPropagation(); var id = parseInt(this.dataset.viewDelete); if (!confirm('Delete this view?')) return; fetch('/db/views/'+id+'/api', {method:'DELETE', headers:{'X-CSRF-Token':getCsrf()}}).then(function(){ state.views = state.views.filter(function(v){ return v.id !== id; }); if (state.activeViewId === id) applyView(state.views[0] || null); render(); }); }; }); var settings = containerEl.querySelector('[data-view-settings]'); if (settings) settings.onclick = function(e){ e.stopPropagation(); openViewSettings(this); }; var add = containerEl.querySelector('[data-view-add]'); if (add) add.onclick = function(e){ e.stopPropagation(); openAddViewMenu(this); }; containerEl.querySelectorAll('.db-add-page-btn').forEach(function(b){ b.onclick = addNewPage; }); } // ── Table view ── function groupedProperties() { var groups = []; state.properties.forEach(function(p){ var g = p.group_name || ''; var last = groups[groups.length-1]; if (!last || last.name !== g) { last = {name:g, props:[]}; groups.push(last); } last.props.push(p); }); return groups; } function renderTable() { var props = state.properties; var pages = state.pages; var groups = groupedProperties(); var hasGroups = groups.some(function(g){ return g.name; }); var h = '
'; h += ''; if (hasGroups) { h += ''; h += ''; groups.forEach(function(g, gi){ var collapsed = state.collapsedGroups[gi]; h += ''; }); h += ''; h += ''; } h += ''; h += ''; props.forEach(function(prop){ var w = state.colWidths[prop.id] || 200; var groupIdx = 0; groups.forEach(function(g, gi){ if (g.props.indexOf(prop) >= 0) groupIdx = gi; }); var collapsed = hasGroups && state.collapsedGroups[groupIdx]; h += ''; }); h += ''; h += ''; h += ''; pages.forEach(function(cp){ h += ''; h += ''; props.forEach(function(prop){ var groupIdx = 0; groups.forEach(function(g, gi){ if (g.props.indexOf(prop) >= 0) groupIdx = gi; }); var collapsed = hasGroups && state.collapsedGroups[groupIdx]; var val = pageVal(cp, prop); var ro = READONLY_TYPES.indexOf(prop.prop_type) >= 0; h += ''; }); h += ''; h += ''; }); var emptyNeeded = Math.max(0, 4 - pages.length); for (var i=0;i'; h += '
'; if (g.name) { h += '▾'+escHtml(g.name)+''; } h += '
'; h += '
'; h += ''+propTypeSvg(prop.prop_type)+''; h += ''+escHtml(prop.name)+''; h += '
'; h += '
'; h += displayValue(prop, val); if (prop.prop_type === 'date') h += recBadgeHtml(cp); if (prop.prop_type === 'title') h += ''; h += '
'; return h; } // ── Board view ── function optionColumns(prop) { if (!prop) return [{key:'', label:'Items'}]; if (prop.prop_type === 'person') { var cols = (state.members||[]).map(function(m){ return {key:m.login, label:m.full_name||m.login, person:m}; }); cols.push({key:'__none__', label:'No assignee'}); return cols; } var opts = parseOpts(prop).map(function(o){ return {key:o.name, label:o.name, color:o.color}; }); if (!opts.length) { var seen = {}; state.pages.forEach(function(cp){ var v = pageVal(cp, prop); if (v === undefined || v === null || v === '') return; (Array.isArray(v)?v:[v]).forEach(function(x){ if (!seen[x]) { seen[x]=1; opts.push({key:x,label:x}); } }); }); opts.push({key:'__none__', label:'No value'}); } else { opts.push({key:'__none__', label:'No value'}); } return opts; } function columnKeyFor(prop, cp) { var v = pageVal(cp, prop); if (prop.prop_type === 'person') { if (Array.isArray(v) && v.length) return v[0].login || v[0].full_name || '__none__'; return '__none__'; } if (Array.isArray(v)) return v[0] || '__none__'; if (v === undefined || v === null || v === '') return '__none__'; return String(v); } function cardHtml(cp, opts) { opts = opts || {}; var cover = opts.cover !== false ? coverHtml(cp) : ''; var size = state.viewConfig.card_size || 'small'; var cardProps = state.viewConfig.card_properties; if (!Array.isArray(cardProps) || !cardProps.length) cardProps = defaultCardProps(); var body = '
'; body += '
'+(cp.icon&&cp.icon!=='file'?escHtml(cp.icon)+' ':'')+escHtml(cp.title||'Untitled')+'
'; cardProps.forEach(function(ref){ var p = findProp(ref); if (!p) return; var v = pageVal(cp, p); var txt = textValue(p, v); if (!txt) return; body += '
'+escHtml(p.name)+''+escHtml(txt.slice(0,60))+'
'; }); body += '
'; return '
'+cover+body+'
'; } function defaultCardProps() { return state.properties.filter(function(p){ return p.prop_type !== 'title'; }).slice(0,3).map(function(p){ return p.id; }); } function coverHtml(cp) { if (cp.cover_url) return '
'; var mode = state.viewConfig.cover_mode || 'icon'; if (mode === 'none') return ''; if (mode === 'property') { var p = findProp(state.viewConfig.cover_property); if (p) { var v = pageVal(cp, p); var url = null; if (Array.isArray(v) && v.length) url = (v[0] && (v[0].url||v[0].src)) || (typeof v[0]==='string'?v[0]:null); else if (typeof v === 'string' && /^https?:\/\//.test(v)) url = v; if (url) return '
'; } } if (mode === 'color') { var g = groupProp(); var key = g ? columnKeyFor(g, cp) : ''; var col = g ? optColor(g, key) : '#8b8b8b'; return '
'; } return '
'+escHtml(cp.icon && cp.icon!=='file' ? cp.icon : '📄')+'
'; } function renderBoard() { var g = groupProp(); if (!g) return '
Add a Select, Status or Person property to group the board.
'; var sub = findProp(state.viewConfig.sub_group_by); var cols = optionColumns(g); var wip = state.viewConfig.wip_limits || {}; var h = '
'; if (sub) { var lanes = optionColumns(sub); lanes.forEach(function(lane){ var lanePages = state.pages.filter(function(cp){ return columnKeyFor(sub, cp) === lane.key; }); if (!lanePages.length && lane.key === '__none__') return; h += '
'+escHtml(lane.label)+' '+lanePages.length+'
'; h += boardColumnsHtml(g, cols, lanePages, wip); h += '
'; }); } else { h += boardColumnsHtml(g, cols, state.pages, wip); } h += '
'; return h; } function boardColumnsHtml(g, cols, pages, wip) { var h = '
'; cols.forEach(function(col){ var colPages = pages.filter(function(cp){ return columnKeyFor(g, cp) === col.key; }); var limit = wip[col.key]; var exceeded = limit && colPages.length > Number(limit); var dot = col.person ? avatarHtml(col.person) : (col.color ? '' : ''); h += '
'; h += '
'+dot+''+escHtml(col.label)+''+colPages.length+''; if (limit) h += 'WIP '+colPages.length+'/'+limit+''; h += '
'; h += '
'; colPages.forEach(function(cp){ h += cardHtml(cp); }); h += ''; h += '
'; }); h += '
'; return h; } // ── Calendar view (v5.8.0: day/week/month + server-expanded recurrences) ── function dateProp() { return findProp(state.viewConfig.date_property) || firstPropOfTypes(['date']); } var CAL_DOW = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; var MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December']; function isoDay(d) { return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0'); } function mondayOf(d) { var x = new Date(d); x.setDate(x.getDate() - ((x.getDay()+6)%7)); x.setHours(0,0,0,0); return x; } function calMode() { var m = state.viewConfig.calendar_mode; return (m==='day'||m==='week') ? m : 'month'; } function baseEvents() { var dp = dateProp(); if (!dp) return {}; var byDate = {}; state.pages.forEach(function(cp){ var v = pageVal(cp, dp); if (!v) return; var ds = String(v).slice(0,10); (byDate[ds] = byDate[ds] || []).push({page_id:cp.id, title:cp.title, icon:cp.icon, recurring:false, time:String(v).length>10?String(v).slice(11,16):''}); }); return byDate; } function eventsForWindow(startIso, endIso) { // Server expansion covers recurring occurrences; merge over base events. var dp = dateProp(); if (!dp) return Promise.resolve({}); return fetch('/db/'+collectionId+'/calendar/api?start='+startIso+'&end='+endIso+'&date_property='+dp.id, {credentials:'same-origin'}) .then(function(r){ return r.ok ? r.json() : {events:[]}; }) .then(function(d){ var byDate = {}; (d.events||[]).forEach(function(ev){ (byDate[ev.date] = byDate[ev.date] || []).push(ev); }); return {byDate: byDate, timezone: d.timezone || ''}; }) .catch(function(){ return {byDate: baseEvents(), timezone:''}; }); } function evChipHtml(ev) { var badge = ev.recurring ? '\u21bb' : ''; return '
' + (ev.time ? ''+escHtml(ev.time)+'' : '') + (ev.icon && ev.icon !== 'file' ? escHtml(ev.icon)+' ' : '') + escHtml(ev.title || 'Untitled') + badge + '
'; } function renderCalendar() { var dp = dateProp(); if (!dp) return '
Add a Date property to use the calendar view.
'; var mode = calMode(); var cur = state.calCursor; var h = '
'; h += '
'; var title; if (mode === 'month') title = MONTH_NAMES[cur.getMonth()]+' '+cur.getFullYear(); else if (mode === 'week') { var ws=mondayOf(cur); title = MONTH_NAMES[ws.getMonth()].slice(0,3)+' '+ws.getDate()+' \u2013 '+(function(we){return MONTH_NAMES[we.getMonth()].slice(0,3)+' '+we.getDate();})(new Date(ws.getTime()+6*864e5)); } else title = MONTH_NAMES[cur.getMonth()]+' '+cur.getDate()+', '+cur.getFullYear(); h += '
'+title+'
'; h += ''; h += '
'+ ['day','week','month'].map(function(m){ return ''; }).join('')+ '
'; h += '
'; h += '
Loading\u2026
'; return h; } function calWindow() { var mode = calMode(); var cur = state.calCursor; if (mode === 'month') { var first = new Date(cur.getFullYear(), cur.getMonth(), 1); var startDow = (first.getDay()+6)%7; var start = new Date(first.getTime() - startDow*864e5); var daysInMonth = new Date(cur.getFullYear(), cur.getMonth()+1, 0).getDate(); var totalCells = Math.ceil((startDow + daysInMonth)/7)*7; return {start: start, days: totalCells, mode: mode, startDow: startDow, daysInMonth: daysInMonth}; } if (mode === 'week') return {start: mondayOf(cur), days: 7, mode: mode}; return {start: new Date(cur.getFullYear(), cur.getMonth(), cur.getDate()), days: 1, mode: mode}; } function paintCalendar(win, data) { var body = containerEl.querySelector('#db-cal-body'); if (!body) return; body.removeAttribute('data-cal-loading'); var today = new Date(); var todayIso = isoDay(today); var byDate = data.byDate || {}; var h = ''; if (win.mode === 'month') { h += '
'; CAL_DOW.forEach(function(d){ h += '
'+d+'
'; }); for (var i=0;i'; h += '
'+cellDate.getDate()+'
'; (byDate[iso]||[]).forEach(function(ev){ h += evChipHtml(ev); }); h += '
'; } h += ''; } else if (win.mode === 'week') { h += '
'; CAL_DOW.forEach(function(d){ h += '
'+d+'
'; }); for (var j=0;j<7;j++) { var d2 = new Date(win.start.getTime() + j*864e5); var iso2 = isoDay(d2); h += '
'; h += '
'+d2.getDate()+'
'; (byDate[iso2]||[]).forEach(function(ev){ h += evChipHtml(ev); }); h += '
'; } h += '
'; } else { var iso3 = isoDay(win.start); h += '
'; h += '
'+CAL_DOW[(win.start.getDay()+6)%7]+' '+MONTH_NAMES[win.start.getMonth()]+' '+win.start.getDate()+(iso3===todayIso?' \u00b7 today':'')+'
'; (byDate[iso3]||[]).slice().sort(function(a,b){ return (a.time||'99').localeCompare(b.time||'99'); }).forEach(function(ev){ h += '
' + ''+(ev.time || '\u2014')+'' + ''+(ev.icon && ev.icon!=='file' ? escHtml(ev.icon)+' ' : '')+escHtml(ev.title||'Untitled')+'' + (ev.recurring ? '\u21bb' : '') + '
'; }); if (!(byDate[iso3]||[]).length) h += '
No events
'; h += '
'; } body.innerHTML = h; bindCalendarCells(); } var calSeq = 0; function refreshCalendar() { var win = calWindow(); var startIso = isoDay(win.start); var endIso = isoDay(new Date(win.start.getTime() + (win.days-1)*864e5)); var seq = ++calSeq; eventsForWindow(startIso, endIso).then(function(data){ if (seq !== calSeq) return; // a newer request won paintCalendar(win, data); }); } function bindCalendarCells() { // day cells: drop to reschedule containerEl.querySelectorAll('[data-cal-date]').forEach(function(day){ day.addEventListener('dragover', function(e){ e.preventDefault(); this.classList.add('drop-target'); }); day.addEventListener('dragleave', function(){ this.classList.remove('drop-target'); }); day.addEventListener('drop', function(e){ e.preventDefault(); this.classList.remove('drop-target'); var pid = parseInt(e.dataTransfer.getData('text/plain')); var cp = pageById(pid); var dp = dateProp(); if (!cp || !dp) return; var old = pageVal(cp, dp); var newDate = this.dataset.calDate; var time = (old && String(old).length > 10) ? String(old).slice(10) : ''; setPageVal(cp, dp, newDate + time); render(); savePage(cp); }); }); // event chips: open row in peek (stop drag click-through) containerEl.querySelectorAll('.db-cal-ev, .db-cal-agenda-row').forEach(function(ev){ ev.addEventListener('dragstart', function(e){ e.dataTransfer.setData('text/plain', this.dataset.cardPage); this.classList.add('dragging'); }); ev.addEventListener('dragend', function(){ this.classList.remove('dragging'); }); ev.addEventListener('click', function(e){ e.stopPropagation(); openPageInSidePeek(parseInt(this.dataset.cardPage)); }); ev.addEventListener('dblclick', function(e){ e.stopPropagation(); openEventEditor(parseInt(this.dataset.cardPage), this.dataset.evDate, e); }); }); } // ── v5.8.0: event editor popover (Repeat + Remind + time) ── function openEventEditor(pageId, evDate, mouseEvt) { var cp = pageById(pageId); var dp = dateProp(); if (!cp || !dp) return; if (!state.zones) { fetch('/db/timezones/api', {credentials:'same-origin'}).then(function(r){return r.ok?r.json():{zones:[]};}) .then(function(d){ state.zones = d.zones || []; }).catch(function(){}); } var old = document.querySelector('.db-event-pop'); if (old) old.remove(); var pid = String(dp.id); var rec = (metaOf(cp, '__recurrence__') || {})[pid] || null; var rem = (metaOf(cp, '__reminder__') || {})[pid] || null; var baseVal = String(pageVal(cp, dp) || ''); var curTime = baseVal.length >= 16 ? baseVal.slice(11,16) : ''; var pop = document.createElement('div'); pop.className = 'db-event-pop'; var freq = rec ? rec.freq : ''; var curTz = ((metaOf(cp, '__timezone__') || {})[pid]) || ''; var h = '
'+escHtml(cp.title||'Untitled')+'
'; h += '
'; h += '
'; h += '
'; if (freq) { h += '
'; h += '
'; if (rec.count != null) h += '
'; if (rec.until) h += '
'; } h += '
'; h += '
'; h += '
'; pop.innerHTML = h; document.body.appendChild(pop); if (mouseEvt) { pop.style.left = Math.min(mouseEvt.clientX, window.innerWidth-300)+'px'; pop.style.top = Math.min(mouseEvt.clientY, window.innerHeight-320)+'px'; } else { pop.style.left='50%'; pop.style.top='120px'; } var freqSel = pop.querySelector('[data-ev-freq]'); function collectDraft() { var draft = rec ? Object.assign({}, rec) : {}; draft.freq = freqSel.value; var iv = pop.querySelector('[data-ev-interval]'); if (iv) draft.interval = parseInt(iv.value)||1; if (!pop.querySelector('[data-ev-interval]')) draft.interval = draft.interval || 1; var em = pop.querySelector('[data-ev-endmode]'); if (em) { if (em.value === 'count') { var c=pop.querySelector('[data-ev-count]'); draft.count = c ? (parseInt(c.value)||null) : null; draft.until = null; } else if (em.value === 'until') { var u=pop.querySelector('[data-ev-until]'); draft.until = (u&&u.value) ? u.value : null; draft.count = null; } else { draft.count = null; draft.until = null; } } if (!draft.freq) return null; return draft; } freqSel.onchange = function(){ // re-open to reveal/hide detail rows, keeping the in-place edits setMeta(cp, '__recurrence__', dp.id, collectDraft()); rec = (metaOf(cp, '__recurrence__') || {})[pid] || null; pop.remove(); openEventEditor(pageId, evDate, mouseEvt); }; var endSel = pop.querySelector('[data-ev-endmode]'); if (endSel) endSel.onchange = function(){ setMeta(cp, '__recurrence__', dp.id, collectDraft()); rec = (metaOf(cp, '__recurrence__') || {})[pid] || null; pop.remove(); openEventEditor(pageId, evDate, mouseEvt); }; pop.querySelector('[data-ev-save]').onclick = function(){ var base = (baseVal.slice(0,10)) || evDate || isoDay(new Date()); var t = pop.querySelector('[data-ev-time]').value; var recDraft = collectDraft(); var tzVal = pop.querySelector('[data-ev-tz]').value; var unit = pop.querySelector('[data-ev-remunit]').value; var val = parseInt(pop.querySelector('[data-ev-remval]').value)||0; setPageVal(cp, dp, t && base ? (base+'T'+t) : base); setMeta(cp, '__recurrence__', dp.id, recDraft); setMeta(cp, '__timezone__', dp.id, tzVal || null); setMeta(cp, '__reminder__', dp.id, unit==='none'?null:{value:val||1, unit:unit}); pop.remove(); render(); savePage(cp); }; setTimeout(function(){ document.addEventListener('click', function onDoc(e){ if (!pop.contains(e.target)) { pop.remove(); document.removeEventListener('click', onDoc); } }, {once:true}); }, 0); } // ── Gallery view ── function renderGallery() { var size = state.viewConfig.card_size || 'medium'; var h = ''; return h; } // ── List view ── function renderList() { var cols = state.viewConfig.visible_properties; var props; if (Array.isArray(cols) && cols.length) props = cols.map(findProp).filter(Boolean); else props = state.properties.filter(function(p){ return p.prop_type !== 'title'; }).slice(0,3); var h = '
'; state.pages.forEach(function(cp){ h += '
'; h += ''+escHtml(cp.icon&&cp.icon!=='file'?cp.icon:'📄')+''; h += ''+escHtml(cp.title||'Untitled')+''; props.forEach(function(p){ h += ''+escHtml(textValue(p, pageVal(cp,p)).slice(0,40))+''; }); h += '
'; }); h += '
'; return h; } // ── Event binding ── function bindEvents() { bindViewBar(); containerEl.querySelectorAll('.db-cell-content').forEach(function(el){ el.onclick = function(e){ if (state.editingCell || e.target.closest('.db-open-btn') || e.target.closest('a')) return; startEditCell(parseInt(this.dataset.pageId), parseInt(this.dataset.propId), this); }; }); containerEl.querySelectorAll('.db-open-page-btn').forEach(function(btn){ btn.onclick = function(e){ e.stopPropagation(); openPageInSidePeek(parseInt(this.dataset.pageId)); }; }); containerEl.querySelectorAll('.db-ai-fill-btn').forEach(function(btn){ btn.onclick = function(e){ e.stopPropagation(); aiFillRow(parseInt(this.dataset.pageId)); }; }); containerEl.querySelectorAll('.db-row').forEach(function(row){ row.onmouseenter = function(){ this.querySelectorAll('.db-hover-only').forEach(function(el){ el.style.visibility='visible'; }); }; row.onmouseleave = function(){ this.querySelectorAll('.db-hover-only').forEach(function(el){ el.style.visibility='hidden'; }); }; }); containerEl.querySelectorAll('.db-th-content').forEach(function(el){ el.onclick = function(e){ e.stopPropagation(); showColMenu(parseInt(this.dataset.propId), this.dataset.propType, this.dataset.propName, this); }; }); containerEl.querySelectorAll('.db-col-resize-handle').forEach(bindResize); var addProp = containerEl.querySelector('.db-show-prop-modal-btn'); if (addProp) addProp.onclick = showPropModal; // property group collapse containerEl.querySelectorAll('[data-group-toggle]').forEach(function(el){ el.onclick = function(){ var gi = parseInt(this.dataset.groupToggle); state.collapsedGroups[gi] = !state.collapsedGroups[gi]; render(); }; }); // open cards / rows containerEl.querySelectorAll('[data-card-page]').forEach(function(el){ el.addEventListener('click', function(e){ if (e.target.closest('[data-add-col]')) return; openPageInSidePeek(parseInt(this.dataset.cardPage)); }); el.addEventListener('dragstart', function(e){ e.dataTransfer.setData('text/plain', this.dataset.cardPage); this.classList.add('dragging'); }); el.addEventListener('dragend', function(){ this.classList.remove('dragging'); }); }); // board drop targets containerEl.querySelectorAll('[data-drop-col]').forEach(function(zone){ zone.addEventListener('dragover', function(e){ e.preventDefault(); this.classList.add('drop-target'); }); zone.addEventListener('dragleave', function(){ this.classList.remove('drop-target'); }); zone.addEventListener('drop', function(e){ e.preventDefault(); this.classList.remove('drop-target'); var pid = parseInt(e.dataTransfer.getData('text/plain')); var cp = pageById(pid); var g = groupProp(); if (!cp || !g) return; var key = this.dataset.dropCol; var val = key === '__none__' ? '' : key; if (g.prop_type === 'person') { var m = (state.members||[]).filter(function(x){ return (x.login||x.full_name) === key; })[0]; val = m ? [m] : []; } else if (g.prop_type === 'multi_select') { val = key === '__none__' ? [] : [key]; } setPageVal(cp, g, val); render(); savePage(cp); }); }); // board add card containerEl.querySelectorAll('[data-add-col]').forEach(function(b){ b.onclick = function(e){ e.stopPropagation(); addNewPage(this.dataset.addCol); }; }); // calendar nav (v5.8.0: mode-aware) function stepCursor(dir) { var c = state.calCursor; if (calMode() === 'month') state.calCursor = new Date(c.getFullYear(), c.getMonth()+dir, 1); else if (calMode() === 'week') { var w = new Date(c); w.setDate(w.getDate()+7*dir); state.calCursor = w; } else { var d = new Date(c); d.setDate(d.getDate()+dir); state.calCursor = d; } render(); } var prev = containerEl.querySelector('[data-cal-prev]'); if (prev) prev.onclick = function(){ stepCursor(-1); }; var next = containerEl.querySelector('[data-cal-next]'); if (next) next.onclick = function(){ stepCursor(1); }; var today = containerEl.querySelector('[data-cal-today]'); if (today) today.onclick = function(){ state.calCursor = new Date(); render(); }; containerEl.querySelectorAll('[data-cal-mode]').forEach(function(b){ b.onclick = function(){ var m = this.dataset.calMode; if (m !== calMode()) { persistConfig({calendar_mode: m}, {silent:true}); render(); } }; }); if (state.viewType === 'calendar' && containerEl.querySelector('#db-cal-body')) refreshCalendar(); var galAdd = containerEl.querySelector('[data-gallery-add]'); if (galAdd) galAdd.onclick = function(){ addNewPage(); }; } function bindResize(handle) { handle.onpointerdown = function(e) { e.preventDefault(); e.stopPropagation(); var propId = parseInt(this.dataset.propId); var th = this.parentElement; var thLeft = th.getBoundingClientRect().left; function onMove(ev) { var newW = Math.max(60, ev.clientX - thLeft); state.colWidths[propId] = newW; th.style.width = newW+'px'; th.style.minWidth = newW+'px'; var colIdx = state.properties.findIndex(function(p){ return p.id === propId; }); if (colIdx >= 0) containerEl.querySelectorAll('.db-row').forEach(function(row){ var cells = row.querySelectorAll('.db-td-cell'); if (cells[colIdx]) cells[colIdx].style.minWidth = newW+'px'; }); } function onUp() { document.removeEventListener('pointermove',onMove); document.removeEventListener('pointerup',onUp); document.body.style.cursor=''; document.body.style.userSelect=''; } document.body.style.cursor='col-resize'; document.body.style.userSelect='none'; document.addEventListener('pointermove',onMove); document.addEventListener('pointerup',onUp); }; } // ── Cell editing ── function startEditCell(pageId, propId, cellEl) { var cp = pageById(pageId); var prop = propsById()[String(propId)]; if (!cp || !prop || READONLY_TYPES.indexOf(prop.prop_type) >= 0) return; var val = pageVal(cp, prop); var t = prop.prop_type; if (t === 'person') { openPersonPicker(cellEl, cp, prop); return; } var input; cellEl.innerHTML = ''; cellEl.classList.add('db-cell-editing'); if (t === 'checkbox') { setPageVal(cp, prop, !val); render(); savePage(cp); return; } else if (t === 'select' || t === 'status') { input = document.createElement('select'); input.className = 'db-cell-input'; input.innerHTML = '' + parseOpts(prop).map(function(o){ return ''+escHtml(o.name)+''; }).join(''); } else if (t === 'date') { input = document.createElement('input'); input.type = 'date'; input.className = 'db-cell-input'; input.value = val ? String(val).slice(0,10) : ''; } else if (t === 'number') { input = document.createElement('input'); input.type = 'number'; input.className = 'db-cell-input'; input.value = val==null?'':val; } else { input = document.createElement('input'); input.type = 'text'; input.className = 'db-cell-input'; input.value = textValue(prop, val); } cellEl.appendChild(input); input.focus(); if (input.select) input.select(); state.editingCell = {pageId:pageId, propId:propId, input:input, cellEl:cellEl, prop:prop}; input.onkeydown = function(e){ if (e.key === 'Enter') commitEditCell(); else if (e.key === 'Escape') cancelEditCell(); }; input.onblur = function(){ commitEditCell(); }; } function commitEditCell() { if (!state.editingCell) return; var ed = state.editingCell; state.editingCell = null; var cp = pageById(ed.pageId); var prop = ed.prop; var val = ed.input.value; if (prop.prop_type === 'multi_select') val = val ? val.split(',').map(function(s){ return s.trim(); }).filter(Boolean) : []; if (prop.prop_type === 'number' && val !== '') val = Number(val); if (prop.prop_type === 'checkbox') val = !!val; setPageVal(cp, prop, val); render(); savePage(cp, ed.cellEl); } function cancelEditCell() { state.editingCell = null; render(); } function openPersonPicker(cellEl, cp, prop) { var old = document.querySelector('.db-person-picker'); if (old) old.remove(); var cur = pageVal(cp, prop); if (!Array.isArray(cur)) cur = []; var menu = document.createElement('div'); menu.className = 'db-person-picker'; menu.innerHTML = (state.members||[]).map(function(m){ var checked = cur.some(function(x){ return String(x.id) === String(m.id); }); return '
'+avatarHtml(m)+''+escHtml(m.full_name||m.login)+''+(checked?'✓':'')+'
'; }).join('') || '
No members
'; document.body.appendChild(menu); var r = cellEl.getBoundingClientRect(); menu.style.left = r.left+'px'; menu.style.top = (r.bottom+4)+'px'; menu.querySelectorAll('[data-mid]').forEach(function(opt){ opt.onclick = function(){ var m = (state.members||[]).filter(function(x){ return String(x.id) === String(this.dataset.mid); }.bind(this))[0]; var idx = cur.findIndex(function(x){ return String(x.id) === String(m.id); }); if (idx >= 0) cur.splice(idx,1); else cur.push(m); setPageVal(cp, prop, cur.slice()); menu.remove(); render(); savePage(cp); }; }); setTimeout(function(){ document.addEventListener('click', function onDoc(e){ if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', onDoc); } }, {once:true}); }, 0); } function savePage(cp, cellEl) { var pv = pvOf(cp); return fetch('/db/pages/'+cp.id+'/api', { method:'PUT', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({title: cp.title||'', properties: pv}) }).then(function(r){ if (!r.ok) return r.json().then(function(d){ throw {status:r.status, detail:(d&&d.detail)||('Error '+r.status)}; }); return r.json(); }).catch(function(err){ var msg = (err && err.detail) ? String(err.detail) : ((err&&err.message)||'Failed to save'); if (cellEl && err && err.status === 400) { cellEl.classList.add('db-cell-invalid'); cellEl.title = msg; } toast('⚠ '+msg, 'error'); }); } // ── v5.9.0: AI property suggestions ── function aiFillRow(pageId) { var cp = pageById(pageId); if (!cp) return; var props = state.properties.filter(function(p){ return p.prop_type !== 'title' && READONLY_TYPES.indexOf(p.prop_type) < 0; }); if (!props.length) { toast('Aucune propriété à remplir'); return; } toast('✨ FlowDeck AI : analyse en cours…'); fetch('/api/agent/writing/properties', { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({title: cp.title||'', content: cp.content||'', properties: props.map(function(p){ return {name:p.name, type:p.prop_type}; })}) }).then(function(r){ return r.json(); }).then(function(d){ if (!d || !d.ok) { toast((d&&d.error)||'Échec de la génération'); return; } var filled = 0; props.forEach(function(p){ var v = d.suggestions ? d.suggestions[p.name] : undefined; if (v !== undefined && v !== null && v !== '') { setPageVal(cp, p, v); filled++; } }); render(); savePage(cp); toast(filled ? ('✨ '+filled+' propriété(s) remplie(s)') : 'Aucune suggestion disponible'); }).catch(function(){ toast('Erreur IA', 'error'); }); } function addNewPage(colKey) { var body = {title:''}; var g = groupProp(); if (colKey !== undefined && g && colKey !== '__none__') { if (g.prop_type === 'person') { var m = (state.members||[]).filter(function(x){ return (x.login||x.full_name) === colKey; })[0]; body.properties = {}; body.properties[String(g.id)] = m ? [m] : []; } else if (g.prop_type === 'multi_select') { body.properties = {}; body.properties[String(g.id)] = [colKey]; } else { body.properties = {}; body.properties[String(g.id)] = colKey; } } fetch('/api/collections/'+collectionId+'/pages', {method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify(body)}) .then(function(r){ return r.json(); }) .then(function(d){ if (d.id) { state.pages.push({id:d.id,title:'',icon:'file',collection_id:collectionId,property_values_json:d.property_values_json||body.properties||{}}); render(); } }) .catch(function(e){ console.error('[DB] addNewPage error:', e); }); } // ── Property modal ── function showPropModal() { var old = containerEl.querySelector('.db-prop-modal-inline'); if (old) old.remove(); var modal = document.createElement('div'); modal.className = 'db-prop-modal-inline'; modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:1000;display:flex;align-items:center;justify-content:center'; modal.innerHTML = '
'+ '

Add a property

'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'; document.body.appendChild(modal); var nameInput = modal.querySelector('.db-prop-name-inline'); nameInput.focus(); modal.querySelector('.db-prop-modal-close-btn').onclick = function(){ modal.remove(); }; modal.querySelector('.db-prop-cancel-inline').onclick = function(){ modal.remove(); }; modal.onclick = function(e){ if (e.target === modal) modal.remove(); }; modal.querySelector('.db-prop-create-inline').onclick = function(){ var name = nameInput.value.trim(); if (!name) return; var type = modal.querySelector('.db-prop-type-inline').value; var group = modal.querySelector('.db-prop-group-inline').value.trim(); var validation = {}; if (modal.querySelector('.db-prop-required-inline').checked) validation.required = true; if (modal.querySelector('.db-prop-unique-inline').checked) validation.unique = true; var min = modal.querySelector('.db-prop-min-inline').value; var max = modal.querySelector('.db-prop-max-inline').value; if (min !== '') validation.min = Number(min); if (max !== '') validation.max = Number(max); fetch('/db/'+collectionId+'/properties/api', {method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({name:name, prop_type:type, group_name:group, validation:validation})}) .then(function(r){ return r.json(); }) .then(function(d){ if (d.id) { d.options_json = d.options_json || '[]'; d.group_name = group; state.properties.push(d); render(); modal.remove(); } }) .catch(function(e){ console.error('[DB] createProperty error:', e); }); }; nameInput.onkeydown = function(e){ if (e.key === 'Enter') modal.querySelector('.db-prop-create-inline').click(); }; } // ── Column context menu ── function showColMenu(propId, propType, propName, anchor) { var old = document.querySelector('.db-col-menu-inline'); if (old) old.remove(); var menu = document.createElement('div'); menu.className = 'db-col-menu-inline'; menu.style.cssText = 'position:fixed;min-width:240px;max-width:320px;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.14);z-index:800;padding:4px 0;font-size:13px;color:var(--text-primary)'; var rect = anchor.getBoundingClientRect(); menu.style.top = (rect.bottom+4)+'px'; menu.style.left = rect.left+'px'; menu.innerHTML = '
'+propTypeSvg(propType)+''+escHtml(propName)+'ⓘ
'+ '
'+ '
≡ Filter
'+ '
↕ Sort
'+ '
▦ Group by this
'+ '
🗂 Move to group…
'+ '
'+ '
├← Insert left
'+ '
→┤ Insert right
'; document.body.appendChild(menu); menu.querySelectorAll('[data-cm]').forEach(function(item){ item.onclick = function(){ var act = this.dataset.cm; menu.remove(); if (act === 'group') { persistConfig({group_by: propId}); toast('Grouped by '+propName); } else if (act === 'groupname') { var g = prompt('Group name for "'+propName+'" (empty to ungroup):', (findProp(propId)||{}).group_name || ''); if (g !== null) { fetch('/db/properties/'+propId+'/api', {method:'PUT', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({group_name:g.trim()})}) .then(function(){ var p = findProp(propId); if (p) p.group_name = g.trim(); render(); }); } } }; }); setTimeout(function(){ document.addEventListener('click', function onDoc(e){ if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', onDoc); } }, {once:true}); }, 0); } // ── View settings popover ── function closePop() { var p = document.querySelector('.db-view-pop'); if (p) p.remove(); } function openPop(anchor, html) { closePop(); var pop = document.createElement('div'); pop.className = 'db-view-pop'; pop.innerHTML = html; document.body.appendChild(pop); var r = anchor.getBoundingClientRect(); pop.style.left = Math.min(r.left, window.innerWidth - 380) + 'px'; pop.style.top = (r.bottom + 6) + 'px'; setTimeout(function(){ document.addEventListener('click', function onDoc(e){ if (!pop.contains(e.target) && !anchor.contains(e.target)) { pop.remove(); document.removeEventListener('click', onDoc); } }, {once:true}); }, 0); return pop; } function propSelect(name, selected, filterFn) { var opts = state.properties.filter(filterFn || function(){ return true; }).map(function(p){ return ''; }).join(''); return ''; } function openViewSettings(anchor) { var v = activeView(); var t = state.viewType; var h = '
'+viewSvg(t)+' View settings
'; h += '
'; if (t === 'board') { var g = groupProp(); h += '
'+propSelect('group_by', state.viewConfig.group_by || (g?g.id:''), function(p){ return ['select','status','person','multi_select','text'].indexOf(p.prop_type)>=0; })+'
'; h += '
'+propSelect('sub_group_by', state.viewConfig.sub_group_by, function(p){ return ['select','status','person'].indexOf(p.prop_type)>=0; })+'
'; h += '
'; h += '
'; if (state.viewConfig.cover_mode === 'property') h += '
'+propSelect('cover_property', state.viewConfig.cover_property, function(p){ return ['files','url'].indexOf(p.prop_type)>=0; })+'
'; h += '
WIP limits (leave empty for none)
'; optionColumns(g).forEach(function(col){ if (col.key === '__none__') return; var lim = (state.viewConfig.wip_limits||{})[col.key] || ''; h += '
'; }); h += '
Card properties
'; state.properties.filter(function(p){ return p.prop_type!=='title'; }).forEach(function(p){ var on = (state.viewConfig.card_properties||defaultCardProps()).some(function(x){ return String(x)===String(p.id); }); h += ''+escHtml(p.name)+''; }); h += '
'; } else if (t === 'calendar') { h += '
'+propSelect('date_property', state.viewConfig.date_property, function(p){ return p.prop_type==='date'; })+'
'; } else if (t === 'gallery') { h += '
'; h += '
'; if (state.viewConfig.cover_mode === 'property') h += '
'+propSelect('cover_property', state.viewConfig.cover_property, function(p){ return ['files','url'].indexOf(p.prop_type)>=0; })+'
'; h += '
Card properties
'; state.properties.filter(function(p){ return p.prop_type!=='title'; }).forEach(function(p){ var on = (state.viewConfig.card_properties||defaultCardProps()).some(function(x){ return String(x)===String(p.id); }); h += ''+escHtml(p.name)+''; }); h += '
'; } else if (t === 'table') { h += '
Assign properties to collapsible groups. Group headers appear above the table.
'; state.properties.forEach(function(p){ h += '
'; }); } else { h += '
List uses the first properties. Configure card properties in a Board or Gallery view.
'; } h += '
'; h += '
⧉ Duplicate view
'; h += '
🗑 Delete view
'; var pop = openPop(anchor, h); // Wire name var nameInput = pop.querySelector('[data-pop-name]'); if (nameInput) nameInput.onchange = function(){ if (v) { persistConfig({}, {name:this.value, silent:true}); render(); } }; // Wire selects / inputs pop.querySelectorAll('[data-pop-field]').forEach(function(el){ el.onchange = function(){ var patch = {}; patch[this.dataset.popField] = this.value; persistConfig(patch); }; }); pop.querySelectorAll('[data-wip]').forEach(function(el){ el.onchange = function(){ var limits = Object.assign({}, state.viewConfig.wip_limits || {}); if (this.value === '') delete limits[this.dataset.wip]; else limits[this.dataset.wip] = Number(this.value); persistConfig({wip_limits: limits}); }; }); pop.querySelectorAll('[data-cardprop]').forEach(function(el){ el.onclick = function(){ var arr = (state.viewConfig.card_properties || defaultCardProps()).slice(); var id = Number(this.dataset.cardprop); var idx = arr.findIndex(function(x){ return String(x)===String(id); }); if (idx >= 0) arr.splice(idx,1); else arr.push(id); persistConfig({card_properties: arr}); }; }); pop.querySelectorAll('[data-groupassign]').forEach(function(el){ el.onchange = function(){ fetch('/db/properties/'+this.dataset.groupassign+'/api', {method:'PUT', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({group_name:this.value.trim()})}) .then(function(){ var p = findProp(this.dataset.groupassign); if (p) p.group_name = this.value.trim(); }.bind(this)) .then(function(){ render(); }); }; }); pop.querySelectorAll('[data-pop-action]').forEach(function(el){ el.onclick = function(){ var act = this.dataset.popAction; pop.remove(); if (act === 'duplicate' && v) { fetch('/db/views/'+v.id+'/duplicate', {method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body:'{}'}) .then(function(r){ return r.json(); }) .then(function(d){ if (d.id) { state.views.push(d); applyView(d); render(); } }); } else if (act === 'delete' && v) { if (!confirm('Delete this view?')) return; fetch('/db/views/'+v.id+'/api', {method:'DELETE', headers:{'X-CSRF-Token':getCsrf()}}).then(function(){ state.views = state.views.filter(function(x){ return x.id !== v.id; }); applyView(state.views[0] || null); render(); }); } }; }); } function openAddViewMenu(anchor) { var h = '
Add a view
'; ['table','board','calendar','gallery','list'].forEach(function(t){ h += '
'+viewSvg(t)+' '+t.charAt(0).toUpperCase()+t.slice(1)+'
'; }); var pop = openPop(anchor, h); pop.querySelectorAll('[data-addview]').forEach(function(el){ el.onclick = function(){ var t = this.dataset.addview; pop.remove(); fetch('/db/'+collectionId+'/views/save-as', {method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()}, body: JSON.stringify({name:t.charAt(0).toUpperCase()+t.slice(1), view_type:t, config:{}})}) .then(function(r){ return r.json(); }) .then(function(d){ if (d.id) { state.views.push(d); applyView(d); render(); } }); }; }); } // ── Init ── function loadMembers() { if (state.members) return Promise.resolve(state.members); return fetch('/db/'+collectionId+'/members/api').then(function(r){ return r.json(); }).then(function(d){ state.members = (d && d.members) || []; return state.members; }).catch(function(){ state.members = []; }); } if (!initialData) { fetch('/api/collections/'+collectionId+'/table-data').then(function(r){ return r.json(); }).then(function(d){ state.collection = d.collection || {}; state.properties = d.properties || []; state.pages = d.pages || []; state.views = d.views || []; ensureView().then(loadMembers).then(render); }).catch(function(e){ console.error('[DB] load error:', e); }); } else { ensureView().then(loadMembers).then(render); } } // ── Global API ── window.FlowDeckDB = { /** Render a database table into a container element. */ renderInto: function(containerEl, collectionId, initialData) { new DBInstance(containerEl, collectionId, initialData); } }; // ── Full-page init (when page_editor_collection.html is used) ── var PAGE_COLLECTION_ID = window.__DB_COLLECTION_ID || 0; if (PAGE_COLLECTION_ID > 0) { var container = document.getElementById('db-table-container'); if (container) { new DBInstance(container, PAGE_COLLECTION_ID, DB_CONFIG); } } // ── Side peek : panneau latéral des lignes de base (vanilla JS) ── // Le panneau vit dans _database_table.html (page de base). Une base // EMBARQUÉE dans un document n'a pas ce markup : on le crée à la demande // (comme mt-peek de My Tasks), sinon l'icône Open de fin de ligne était // un no-op silencieux. Il doit être câblé SUR le document, jamais via // container.__x : le conteneur n'a pas de x-data. var PEEK_HTML = '
' + '
' + '' + '
' + '' + '
' + '
' + '
Chargement…
' + '' + '
'; function ensureDbPeek() { var p = document.getElementById('db-side-peek'); if (p) return p; p = document.createElement('div'); p.id = 'db-side-peek'; p.className = 'db-side-peek'; p.innerHTML = PEEK_HTML; document.body.appendChild(p); document.getElementById('db-side-peek-close').addEventListener('click', closeDbPeek); document.getElementById('db-side-peek-full').addEventListener('click', function () { var f = document.getElementById('db-side-peek-iframe'); // `dataset.page` et non `iframe.src` : le src porte `?embed=1`, // la pleine page doit garder sa sidebar et sa barre. if (f && f.dataset.page) window.location.href = '/pages/' + f.dataset.page; }); if (window.fdWirePeekResize) window.fdWirePeekResize(p, p.querySelector('.peek-resize-handle'), closeDbPeek); return p; } function closeDbPeek() { var p = document.getElementById('db-side-peek'); if (!p) return; p.classList.remove('open'); document.getElementById('db-side-peek-iframe').src='about:blank'; var t = document.getElementById('db-side-peek-title'); if (t) t.textContent = ''; } window.openPageInSidePeek = function(pageId) { if (!pageId) return; var peekEl = ensureDbPeek(); var peekFrame = document.getElementById('db-side-peek-iframe'); var peekTitle = document.getElementById('db-side-peek-title'); var peekLoading = document.getElementById('db-side-peek-loading'); peekEl.classList.add('open'); if (peekLoading) peekLoading.style.display = ''; if (peekFrame) peekFrame.src = 'about:blank'; if (peekTitle) peekTitle.textContent = ''; resolveRowContentPage(pageId).then(function(pid){ if (!peekEl.classList.contains('open')) return; if (peekLoading) peekLoading.style.display = 'none'; // `?embed=1` : uniquement le document edite, pas l'habillage // (sidebar + barre) — la pleine page reste via le bouton « full ». if (peekFrame) { peekFrame.dataset.page = pid; peekFrame.src = '/pages/'+pid+'?embed=1'; } }) .catch(function(){ // L'échec est rendu DANS le panneau (srcdoc) : sous le build // CSP d'Alpine les toasts ne s'affichent pas partout. if (peekFrame) { peekFrame.srcdoc = '

Impossible d’ouvrir le contenu de cette ligne

'; } peekEl.classList.add('open'); toast('Impossible d’ouvrir le contenu de cette ligne', 'error'); }); }; document.addEventListener('keydown', function(e) { var p = document.getElementById('db-side-peek'); if (e.key === 'Escape' && p && p.classList.contains('open')) closeDbPeek(); }); document.addEventListener('pointerdown', function(e) { var p = document.getElementById('db-side-peek'); if (!p || !p.classList.contains('open')) return; if (p.contains(e.target)) return; if (e.target.closest('#db-table-container, .database-table-container, [id^="inline-db-"]')) return; closeDbPeek(); }); function resolveRowContentPage(rowId){ return fetch('/db/pages/'+rowId+'/open/api') .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function(d){ if (!d || !d.page_id) throw new Error('page de contenu introuvable'); return d.page_id; }); } })(); }