Files
flowdeck/app/templates/_database_table_scripts.html
T
bruno f09de98406
FlowDeck CI / test (push) Failing after 25s
FlowDeck CI / docker (push) Skipped
feat(v5.9.0): AI Writing Assist - slash /ai, autocompletion, AI properties
- Service app/services/ai_writing.py: 6 actions sans outils (write, summarize,
  translate, continue, autocomplete, properties) + replis deterministes offline
- Endpoints POST /api/agent/writing et /api/agent/writing/properties
- Editeur: groupe slash AI (Write/Summarize/Translate/Continue) via E.aiSlash
- Autocompletion inline AIAC (suggestion ~900ms, Tab accepte, Escape rejette)
- Database: bouton AI fill (suggestions de proprietes Status/Priority/Resume)
- Tests tests/test_ai_writing.py (+29) ; version 5.9.0 (VERSION, main.py)
- CHANGELOG + ROADMAP v5.9.0 completes
2026-09-10 11:24:21 -04:00

473 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- ═══════════ Database Table Scripts — Vanilla JS ═══════════ -->
<style>.db-cell-invalid{outline:2px solid #e5484d;outline-offset:-1px;border-radius:4px}</style>
<script>
(function() {
'use strict';
// ── SVG Outline Icons ──
function propTypeSvg(type) {
const s = 14;
switch(type) {
case 'text': case 'title':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg>';
case 'number':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="6" y1="4" x2="18" y2="20"/><line x1="18" y1="4" x2="6" y2="20"/></svg>';
case 'select':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><circle cx="4" cy="6" r="1.5"/><circle cx="4" cy="12" r="1.5"/><circle cx="4" cy="18" r="1.5"/></svg>';
case 'multi_select':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><rect x="2" y="5" width="4" height="2" rx="0.5"/><rect x="2" y="11" width="4" height="2" rx="0.5"/><rect x="2" y="17" width="4" height="2" rx="0.5"/></svg>';
case 'date':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
case 'checkbox':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
case 'url':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
case 'email':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>';
case 'phone':
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>';
default:
return '<svg width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg>';
}
}
function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function cellValue(cpage, prop) {
if (!cpage.property_values_json) return '';
let pv;
try { pv = typeof cpage.property_values_json === 'string' ? JSON.parse(cpage.property_values_json) : cpage.property_values_json; }
catch(e) { return ''; }
const val = pv[String(prop.id)];
if (val === undefined || val === null) return prop.prop_type === 'title' ? (cpage.title || '') : '';
if (Array.isArray(val)) return val.join(', ');
return String(val);
}
// ── DB Instance (one per container) ──
function DBInstance(containerEl, collectionId, initialData) {
const state = {
properties: initialData ? initialData.properties : [],
pages: initialData ? initialData.pages : [],
editingCell: null,
colMenuPropId: null,
colMenuSubOpen: null,
colMenuIconOn: true,
colWidths: {}
};
function getCsrf() { const m = document.cookie.match(/csrf_token=([^;]+)/); return m ? m[1] : ''; }
function render() {
const props = state.properties;
const pages = state.pages;
// Init col widths
for (const p of props) { if (!state.colWidths[p.id]) state.colWidths[p.id] = 200; }
// Build inner HTML
let h = '<div class="db-view-bar" style="padding:4px 0 8px">';
h += '<div class="db-view-tabs"><button class="db-view-tab active"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> Table</button></div>';
h += '<div class="db-view-actions"><button class="db-btn-new db-add-page-btn">+ New</button></div>';
h += '</div>';
h += '<div class="db-table-wrapper"><table class="db-table">';
// Header
h += '<thead><tr class="db-header-row">';
h += '<th class="db-th db-th-handle" style="width:32px;min-width:32px"></th>';
for (const prop of props) {
const w = state.colWidths[prop.id] || 200;
h += '<th class="db-th db-th-property" style="min-width:'+w+'px;width:'+w+'px" data-prop-id="'+prop.id+'">';
h += '<div class="db-th-content" data-prop-id="'+prop.id+'" data-prop-type="'+prop.prop_type+'" data-prop-name="'+escHtml(prop.name)+'">';
h += '<span class="db-prop-type-icon">'+propTypeSvg(prop.prop_type)+'</span>';
h += '<span class="db-prop-name db-col-title-btn">'+escHtml(prop.name)+'</span>';
h += '</div><div class="db-col-resize-handle" data-prop-id="'+prop.id+'"></div></th>';
}
h += '<th class="db-th db-th-add-prop"><button class="db-add-prop-btn db-show-prop-modal-btn">+ Add property</button></th>';
h += '<th class="db-th" style="width:72px;min-width:72px"></th>';
h += '</tr></thead>';
// Body
h += '<tbody>';
for (const cp of pages) {
h += '<tr class="db-row" data-page-id="'+cp.id+'">';
h += '<td class="db-td db-td-handle"><span class="db-drag-handle db-hover-only" style="visibility:hidden">⋮⋮</span></td>';
for (const prop of props) {
const val = cellValue(cp, prop) || '\u2014';
const isNameCol = prop.prop_type === 'title';
h += '<td class="db-td db-td-cell" style="min-width:'+(state.colWidths[prop.id]||200)+'px">';
h += '<div class="db-cell-content" data-page-id="'+cp.id+'" data-prop-id="'+prop.id+'">';
h += '<span class="db-cell-value">'+escHtml(val)+'</span>';
// OPEN button inline in first (title) column
if (isNameCol) {
h += '<button class="db-open-btn db-open-page-btn db-hover-only" data-page-id="'+cp.id+'" title="Open" style="visibility:hidden;margin-left:auto;flex-shrink:0">↗</button>';
}
h += '</div></td>';
}
h += '<td class="db-td db-td-open db-hover-only" style="visibility:hidden;white-space:nowrap"><button class="db-ai-btn db-ai-fill-btn" data-page-id="'+cp.id+'" title="Remplir les propriétés avec l\'IA" style="background:none;border:none;cursor:pointer;font-size:14px;padding:2px 4px;color:var(--text-dim)">✨</button><button class="db-open-btn db-open-page-btn" data-page-id="'+cp.id+'" title="Open">↗</button></td>';
h += '</tr>';
}
// Empty rows + New Page at bottom
const emptyNeeded = Math.max(0, 4 - pages.length);
for (let i = 0; i < emptyNeeded; i++) {
h += '<tr class="db-row db-row-empty"><td class="db-td db-td-handle"></td>';
for (let j = 0; j < props.length; j++) h += '<td class="db-td db-td-cell"><div class="db-cell-content"></div></td>';
h += '<td class="db-td db-td-open"></td></tr>';
}
h += '<tr class="db-row db-row-new"><td class="db-td db-td-handle"><span class="db-plus-icon">+</span></td>';
h += '<td class="db-td db-td-name" colspan="'+(Math.max(1,props.length)+1)+'"><button class="db-new-page-btn db-add-page-btn">New page</button></td></tr>';
h += '</tbody></table></div>';
containerEl.innerHTML = h;
// Bind events
bindEvents();
}
function bindEvents() {
// New page buttons
containerEl.querySelectorAll('.db-add-page-btn').forEach(b => b.onclick = addNewPage);
// Cell edit
containerEl.querySelectorAll('.db-cell-content').forEach(el => {
el.onclick = function(e) {
if (state.editingCell || e.target.closest('.db-open-btn')) return;
startEditCell(parseInt(this.dataset.pageId), parseInt(this.dataset.propId), this);
};
});
// Open buttons
containerEl.querySelectorAll('.db-open-page-btn').forEach(btn => {
btn.onclick = function(e) {
e.stopPropagation();
openPageInSidePeek(parseInt(this.dataset.pageId));
};
});
// AI fill properties
containerEl.querySelectorAll('.db-ai-fill-btn').forEach(btn => {
btn.onclick = function(e) {
e.stopPropagation();
aiFillRow(parseInt(this.dataset.pageId));
};
});
// Hover
containerEl.querySelectorAll('.db-row').forEach(row => {
row.onmouseenter = function() { this.querySelectorAll('.db-hover-only').forEach(el => el.style.visibility = 'visible'); };
row.onmouseleave = function() { this.querySelectorAll('.db-hover-only').forEach(el => el.style.visibility = 'hidden'); };
});
// Header context menu
containerEl.querySelectorAll('.db-th-content').forEach(el => {
el.onclick = function(e) { e.stopPropagation(); showColMenu(parseInt(this.dataset.propId), this.dataset.propType, this.dataset.propName, this); };
});
// Column resize
containerEl.querySelectorAll('.db-col-resize-handle').forEach(handle => {
handle.onpointerdown = function(e) {
e.preventDefault(); e.stopPropagation();
const propId = parseInt(this.dataset.propId);
const th = this.parentElement;
const thLeft = th.getBoundingClientRect().left;
function onMove(ev) {
const newW = Math.max(60, ev.clientX - thLeft);
state.colWidths[propId] = newW;
th.style.width = newW+'px'; th.style.minWidth = newW+'px';
const colIdx = state.properties.findIndex(p => p.id === propId);
if (colIdx >= 0) {
containerEl.querySelectorAll('.db-row').forEach(row => {
const 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);
};
});
// Add property modal bind
containerEl.querySelector('.db-show-prop-modal-btn').onclick = function() { showPropModal(); };
}
function startEditCell(pageId, propId, cellEl) {
const cp = state.pages.find(p => p.id === pageId);
const prop = state.properties.find(p => p.id === propId);
if (!cp || !prop) return;
const val = cellValue(cp, prop);
const input = document.createElement('input');
input.type='text'; input.className='db-cell-input'; input.value=val;
cellEl.innerHTML=''; cellEl.appendChild(input); cellEl.classList.add('db-cell-editing'); input.focus();
state.editingCell = {pageId,propId,input,cellEl};
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;
const {pageId,propId,input,cellEl}=state.editingCell; state.editingCell=null;
const val=input.value;
const cp=state.pages.find(p=>p.id===pageId); const prop=state.properties.find(p=>p.id===propId);
if(!cp||!prop) return;
let pv; try{pv=typeof cp.property_values_json==='string'?JSON.parse(cp.property_values_json):cp.property_values_json}catch(e){pv={};}
pv[String(propId)]=val; cp.property_values_json=pv;
if(prop.prop_type==='title') cp.title=val;
cellEl.classList.remove('db-cell-editing');
const isNameCol = prop.prop_type === 'title';
cellEl.innerHTML = '<span class="db-cell-value">'+escHtml(val||'\u2014')+'</span>';
if (isNameCol) {
cellEl.innerHTML += '<button class="db-open-btn db-open-page-btn db-hover-only" data-page-id="'+pageId+'" title="Open" style="visibility:hidden;margin-left:auto;flex-shrink:0">↗</button>';
}
cellEl.onclick = function(e) { if(!state.editingCell && !e.target.closest('.db-open-btn')) startEditCell(pageId,propId,cellEl); };
fetch('/db/pages/'+pageId+'/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)}; });
}
cellEl.classList.remove('db-cell-invalid');
return r.json();
})
.catch(function(err){
// Validation failed (400) → show message + revert the cell.
const msg = (err && err.detail) ? String(err.detail) : ((err&&err.message)||'Failed to save');
if(err && err.status===400){
cellEl.classList.add('db-cell-invalid');
cellEl.title = msg;
}
if(typeof window.showToast==='function') window.showToast('⚠ '+msg,'error');
else alert('⚠ '+msg);
// Revert to the stored value.
const stored = pv[String(propId)];
cellEl.innerHTML = '<span class="db-cell-value">'+escHtml(stored||'\u2014')+'</span>';
if (prop.prop_type === 'title') {
cellEl.innerHTML += '<button class="db-open-btn db-open-page-btn db-hover-only" data-page-id="'+pageId+'" title="Open" style="visibility:hidden;margin-left:auto;flex-shrink:0">↗</button>';
}
cellEl.onclick = function(e) { if(!state.editingCell && !e.target.closest('.db-open-btn')) startEditCell(pageId,propId,cellEl); };
});
}
function cancelEditCell() {
if(!state.editingCell) return;
const {pageId,propId,cellEl}=state.editingCell; state.editingCell=null;
const cp=state.pages.find(p=>p.id===pageId); const prop=state.properties.find(p=>p.id===propId);
if(!cp||!prop) return;
const val=cellValue(cp,prop);
cellEl.classList.remove('db-cell-editing');
const isNameCol = prop.prop_type === 'title';
cellEl.innerHTML = '<span class="db-cell-value">'+escHtml(val||'\u2014')+'</span>';
if (isNameCol) {
cellEl.innerHTML += '<button class="db-open-btn db-open-page-btn db-hover-only" data-page-id="'+pageId+'" title="Open" style="visibility:hidden;margin-left:auto;flex-shrink:0">↗</button>';
}
cellEl.onclick = function(e) { if(!state.editingCell && !e.target.closest('.db-open-btn')) startEditCell(pageId,propId,cellEl); };
}
// ── v5.9.0: AI property suggestions ──
async function aiFillRow(pageId) {
const cp = state.pages.find(p => p.id === pageId);
if (!cp) return;
const props = state.properties.filter(p => p.prop_type !== 'title');
if (!props.length) {
if (typeof window.showToast === 'function') window.showToast('Aucune propriété à remplir');
return;
}
const payload = {
title: cp.title || '',
content: cp.content || '',
properties: props.map(p => ({ name: p.name, type: p.prop_type }))
};
if (typeof window.showToast === 'function') window.showToast('✨ FlowDeck AI : analyse en cours…');
try {
const r = await fetch('/api/agent/writing/properties', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
body: JSON.stringify(payload)
});
const d = await r.json();
if (!d || !d.ok) {
if (typeof window.showToast === 'function') window.showToast((d && d.error) || 'Échec de la génération');
return;
}
let pv;
try { pv = typeof cp.property_values_json === 'string' ? JSON.parse(cp.property_values_json) : (cp.property_values_json || {}); }
catch (e) { pv = {}; }
let filled = 0;
for (const p of props) {
const v = d.suggestions ? d.suggestions[p.name] : undefined;
if (v !== undefined && v !== null && v !== '') { pv[String(p.id)] = v; filled++; }
}
cp.property_values_json = pv;
await fetch('/db/pages/' + pageId + '/api', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
body: JSON.stringify({ title: cp.title, properties: pv })
});
render();
if (typeof window.showToast === 'function') window.showToast(filled ? ('✨ ' + filled + ' propriété(s) remplie(s)') : 'Aucune suggestion disponible');
} catch (e) {
if (typeof window.showToast === 'function') window.showToast('Erreur IA');
}
}
async function addNewPage() {
try {
const r = await fetch('/api/collections/'+collectionId+'/pages',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()},body:JSON.stringify({title:''})});
const d = await r.json();
if(d.id) { state.pages.push({id:d.id,title:'',position:state.pages.length,icon:'file',collection_id:collectionId,property_values_json:{}}); render(); }
} catch(e) { console.error('[DB] addNewPage error:', e); }
}
// ── Property modal (inline, self-contained) ──
function showPropModal() {
// Remove any existing inline modal
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 = '<div class="db-modal" style="background:var(--bg-modal);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,0.15);width:420px;max-width:90vw;overflow:hidden">'+
'<div class="db-modal-header"><h3>Add a property</h3><button class="db-prop-modal-close-btn" style="display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;background:transparent;cursor:pointer;color:var(--text-dim);font-size:18px">×</button></div>'+
'<div class="db-modal-body" style="padding:16px">'+
'<div class="db-modal-field" style="margin-bottom:12px"><label style="display:block;margin-bottom:4px;font-size:12px;color:var(--text-dim)">Name</label><input class="db-prop-name-inline" type="text" placeholder="Property name" style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:6px;font-size:14px;box-sizing:border-box"></div>'+
'<div class="db-modal-field" style="margin-bottom:12px"><label style="display:block;margin-bottom:4px;font-size:12px;color:var(--text-dim)">Type</label><select class="db-prop-type-inline" style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:6px;font-size:14px;box-sizing:border-box"><option value="text">Text</option><option value="number">Number</option><option value="select">Select</option><option value="multi_select">Multi-select</option><option value="date">Date</option><option value="checkbox">Checkbox</option><option value="url">URL</option><option value="email">Email</option><option value="phone">Phone</option></select></div>'+
'<div style="display:flex;gap:16px;margin-bottom:12px">'+
'<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text)"><input class="db-prop-required-inline" type="checkbox"> Required</label>'+
'<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text)"><input class="db-prop-unique-inline" type="checkbox"> Unique</label>'+
'</div>'+
'<div style="display:flex;gap:10px;margin-bottom:12px">'+
'<div style="flex:1"><label style="display:block;margin-bottom:4px;font-size:12px;color:var(--text-dim)">Min</label><input class="db-prop-min-inline" type="number" placeholder="—" style="width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:6px;font-size:13px;box-sizing:border-box"></div>'+
'<div style="flex:1"><label style="display:block;margin-bottom:4px;font-size:12px;color:var(--text-dim)">Max</label><input class="db-prop-max-inline" type="number" placeholder="—" style="width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:6px;font-size:13px;box-sizing:border-box"></div>'+
'</div>'+
'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px"><button class="db-prop-cancel-inline" style="padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-secondary);font-size:13px;cursor:pointer">Cancel</button><button class="db-prop-create-inline" style="padding:6px 14px;border-radius:6px;border:none;background:var(--accent,#2383E2);color:white;font-size:13px;cursor:pointer">Create</button></div>'+
'</div></div>';
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 = async function(){
var name = nameInput.value.trim();
if(!name) return;
var type = modal.querySelector('.db-prop-type-inline').value;
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);
try {
var r = await fetch('/db/'+collectionId+'/properties/api',{
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCsrf()},
body:JSON.stringify({name:name,prop_type:type,validation:validation})
});
var d = await r.json();
if(d.id){ state.properties.push(d); render(); modal.remove(); }
} catch(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 (inline) ──
function showColMenu(propId, propType, propName, anchor) {
// Remove any existing
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 = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;font-weight:600">'+
'<span>'+propTypeSvg(propType)+'</span><span>'+escHtml(propName)+'</span><span style="font-size:11px;color:var(--text-dim)">ⓘ</span></div>'+
'<div style="height:1px;background:var(--border);margin:4px 0"></div>'+
'<div style="padding:6px 12px;cursor:pointer">≡ Filter</div>'+
'<div style="padding:6px 12px;cursor:pointer">↕ Sort</div>'+
'<div style="padding:6px 12px;cursor:pointer">▦ Group</div>'+
'<div style="height:1px;background:var(--border);margin:4px 0"></div>'+
'<div style="padding:6px 12px;cursor:pointer">├← Insert left</div>'+
'<div style="padding:6px 12px;cursor:pointer">→┤ Insert right</div>';
document.body.appendChild(menu);
setTimeout(() => document.addEventListener('click', function onDoc(e) {
if(!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click',onDoc); }
}, {once:true}), 0);
}
// ── Init ──
if (!initialData) {
fetch('/api/collections/'+collectionId+'/table-data').then(r=>r.json()).then(d => {
state.properties = d.properties||[]; state.pages = d.pages||[]; render();
}).catch(e=>console.error('[DB] load error:',e));
} else { 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) ──
const PAGE_COLLECTION_ID = window.__DB_COLLECTION_ID || 0;
if (PAGE_COLLECTION_ID > 0) {
const container = document.getElementById('db-table-container');
if (container) {
{% if collection_data %}
new DBInstance(container, PAGE_COLLECTION_ID, {
properties: {{ collection_data.properties | tojson }},
pages: {{ collection_data.pages | tojson }}
});
{% else %}
new DBInstance(container, PAGE_COLLECTION_ID, null);
{% endif %}
}
}
// ── Global Side Peek for Database Pages (auto-creates panel) ──
function openPageInSidePeek(pageId) {
var panel = document.getElementById('db-side-peek');
if (!panel) {
// Create panel dynamically
panel = document.createElement('div');
panel.id = 'db-side-peek';
panel.style.cssText = 'position:fixed;top:0;right:0;width:560px;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 20px rgba(0,0,0,0.12);z-index:900;display:flex;flex-direction:column';
panel.innerHTML = '<div id="db-side-peek-resize" style="position:absolute;top:0;left:0;width:6px;height:100%;cursor:col-resize;z-index:2"></div>'+
'<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">'+
'<button id="db-side-peek-close" style="display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;background:transparent;cursor:pointer;font-size:18px;color:var(--text-dim)">×</button>'+
'<button id="db-side-peek-full" style="display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;background:transparent;cursor:pointer;font-size:18px;color:var(--text-dim)" title="Open full page">↗</button>'+
'</div>'+
'<iframe id="db-side-peek-iframe" src="" style="width:100%;height:100%;border:none;flex:1"></iframe>';
document.body.appendChild(panel);
document.getElementById('db-side-peek-close').onclick = function(){ panel.style.display='none'; document.getElementById('db-side-peek-iframe').src=''; };
document.getElementById('db-side-peek-full').onclick = function(){
var ifr = document.getElementById('db-side-peek-iframe');
if(ifr&&ifr.src) window.open(ifr.src.replace('?embed=1',''),'_blank');
};
// Resize
var rh = document.getElementById('db-side-peek-resize');
rh.onpointerdown = function(e) {
e.preventDefault(); e.stopPropagation();
var sx = e.clientX, sw = panel.offsetWidth;
function mv(ev) { var w = Math.max(300, Math.min(window.innerWidth*0.9, sw-(ev.clientX-sx))); panel.style.width = w+'px'; }
function up() { document.removeEventListener('pointermove',mv); document.removeEventListener('pointerup',up); document.body.style.cursor=''; }
document.body.style.cursor='col-resize';
document.addEventListener('pointermove',mv); document.addEventListener('pointerup',up);
};
}
var iframe = document.getElementById('db-side-peek-iframe');
if (iframe) { iframe.src = '/pages/'+pageId+'?embed=1'; panel.style.display = 'flex'; }
}
function closeSidePeek() {
var panel = document.getElementById('db-side-peek');
if (panel) { panel.style.display = 'none'; }
var iframe = document.getElementById('db-side-peek-iframe');
if (iframe) { iframe.src = ''; }
}
})();
</script>