fix(database): rewrite table view in vanilla JS (Alpine 3.14.9 compat)
FlowDeck CI / test (push) Failing after 5s
FlowDeck CI / docker (push) Has been skipped

Alpine 3.14.9 evaluates x-data expressions before Alpine.data() registration,
causing 'pages is not defined', 'emptyRows is not defined', etc.
Solution: pure vanilla JS DOM rendering (same pattern as Library fix).
- _database_table.html: static DOM with IDs only, no Alpine directives
- _database_table_scripts.html: vanilla JS state, render, cell editing, modals
- Removes all x-data, x-for, x-show, x-model from database table
This commit is contained in:
2026-07-22 14:40:22 -04:00
parent 24a760c5eb
commit 50273fcb1a
2 changed files with 340 additions and 334 deletions
+24 -136
View File
@@ -1,145 +1,53 @@
<!-- ═══════════ Database Table View (Notion-style) ═══════════ -->
<div x-data="databaseTable()" x-init="init($el)" class="database-table-container">
<!-- ═══════════ Database Table View (Notion-style) — Vanilla JS ═══════════ -->
<div id="db-table-container" class="database-table-container">
<!-- View Selector Bar -->
<div class="db-view-bar">
<div id="db-view-bar" class="db-view-bar">
<div class="db-view-tabs">
<button class="db-view-tab active"
@click="switchView('table')">
{{ fd_icon("table",14) }} Table
<button id="db-view-tab-table" class="db-view-tab active">
{{ fd_icon("grid",14) }} Table
</button>
<button class="db-view-add" @click="addViewMenuOpen=!addViewMenuOpen"
title="Add a view">
<button id="db-view-add-btn" class="db-view-add" title="Add a view">
{{ fd_icon("plus",14) }}
</button>
<!-- View type dropdown -->
<div x-show="addViewMenuOpen" @click.outside="addViewMenuOpen=false"
class="db-dropdown" x-transition>
<div id="db-add-view-dropdown" class="db-dropdown" style="display:none">
<div class="db-dropdown-header">Add a new view</div>
<div class="db-dropdown-item" @click="addViewMenuOpen=false">{{ fd_icon("table",14) }} Table</div>
<div class="db-dropdown-item" @click="addViewMenuOpen=false">{{ fd_icon("columns",14) }} Board</div>
<div class="db-dropdown-item" @click="addViewMenuOpen=false">{{ fd_icon("list",14) }} List</div>
<div class="db-dropdown-item" @click="addViewMenuOpen=false">{{ fd_icon("calendar",14) }} Calendar</div>
<div class="db-dropdown-item">{{ fd_icon("grid",14) }} Table</div>
<div class="db-dropdown-item">{{ fd_icon("grid",14) }} Board</div>
<div class="db-dropdown-item">{{ fd_icon("list",14) }} List</div>
<div class="db-dropdown-item">{{ fd_icon("calendar",14) }} Calendar</div>
</div>
</div>
<div class="db-view-actions">
<button class="db-btn-icon" title="Filter">{{ fd_icon("filter",14) }}</button>
<button class="db-btn-new" @click="addNewPage()">
<button id="db-filter-btn" class="db-btn-icon" title="Filter">{{ fd_icon("search",14) }}</button>
<button id="db-new-btn" class="db-btn-new">
<span class="db-new-icon">{{ fd_icon("plus",12) }}</span> New
</button>
</div>
</div>
<!-- Database Table -->
<div class="db-table-wrapper">
<table class="db-table">
<thead>
<tr class="db-header-row">
<!-- Drag handle column -->
<th class="db-th db-th-handle" style="width:32px"></th>
<!-- Property columns -->
<template x-for="prop in properties" :key="prop.id">
<th class="db-th db-th-property">
<div class="db-th-content">
<span class="db-prop-type-icon" x-text="propTypeIcon(prop.prop_type)"></span>
<span class="db-prop-name" x-text="prop.name"></span>
</div>
</th>
</template>
<!-- Add Property column -->
<th class="db-th db-th-add-prop">
<button class="db-add-prop-btn" @click="addProperty()"
title="Add a property">
{{ fd_icon("plus",14) }} Add property
</button>
</th>
</tr>
</thead>
<tbody>
<!-- New Page row (always first) -->
<tr class="db-row db-row-new">
<td class="db-td db-td-handle">
<span class="db-plus-icon">{{ fd_icon("plus",14) }}</span>
</td>
<td class="db-td db-td-name" colspan="99">
<button class="db-new-page-btn" @click="addNewPage()">
New page
</button>
</td>
</tr>
<!-- Data rows -->
<template x-for="(cpage, idx) in pages" :key="cpage.id">
<tr class="db-row"
@mouseenter="cpage._hover=true"
@mouseleave="cpage._hover=false"
:class="{ 'db-row-selected': cpage._selected }">
<!-- Drag handle / hover actions -->
<td class="db-td db-td-handle">
<span class="db-drag-handle" x-show="cpage._hover"
@mousedown.prevent="startDrag(idx, $event)">
{{ fd_icon("grip-vertical",14) }}
</span>
</td>
<!-- Property cells -->
<template x-for="prop in properties" :key="prop.id">
<td class="db-td db-td-cell">
<div class="db-cell-content"
:class="{'db-cell-editing': editingCell && editingCell.pageId === cpage.id && editingCell.propId === prop.id}"
@click="startEditCell(cpage.id, prop.id, $event)"
@dblclick="openPage(cpage.id)">
<!-- Render property value -->
<span x-show="!(editingCell && editingCell.pageId === cpage.id && editingCell.propId === prop.id)"
x-text="cellValue(cpage, prop) || '—'"
class="db-cell-value"></span>
<!-- Edit mode -->
<input x-show="editingCell && editingCell.pageId === cpage.id && editingCell.propId === prop.id"
x-ref="cellInput"
:value="cellValue(cpage, prop)"
@keydown.enter="commitEditCell(cpage, prop)"
@keydown.escape="cancelEditCell()"
@blur="commitEditCell(cpage, prop)"
class="db-cell-input" />
</div>
</td>
</template>
<!-- Open button on hover -->
<td class="db-td db-td-open" x-show="cpage._hover">
<button class="db-open-btn" @click="openPage(cpage.id)"
title="Open">
{{ fd_icon("external-link",14) }}
</button>
</td>
</tr>
</template>
<!-- Empty rows (fill to 5 total if less than 4 data rows) -->
<template x-for="i in emptyRows()" :key="'e'+i">
<tr class="db-row db-row-empty">
<td class="db-td db-td-handle"></td>
<template x-for="prop in properties" :key="prop.id">
<td class="db-td db-td-cell"><div class="db-cell-content"></div></td>
</template>
<td class="db-td db-td-open"></td>
</tr>
</template>
</tbody>
<div id="db-table-wrapper" class="db-table-wrapper">
<table id="db-table" class="db-table">
<thead id="db-thead"></thead>
<tbody id="db-tbody"></tbody>
</table>
</div>
<!-- Add Property Modal -->
<div x-show="propModalOpen" class="db-modal-overlay" @click.self="propModalOpen=false">
<div class="db-modal" x-transition>
<div id="db-prop-modal" class="db-modal-overlay" style="display:none">
<div class="db-modal">
<div class="db-modal-header">
<h3>Add a property</h3>
<button @click="propModalOpen=false">{{ fd_icon("x",14) }}</button>
<button id="db-prop-modal-close">{{ fd_icon("x",14) }}</button>
</div>
<div class="db-modal-body">
<div class="db-modal-field">
<label>Name</label>
<input x-model="newPropName" type="text" placeholder="Property name"
@keydown.enter="createProperty()" />
<input id="db-prop-name-input" type="text" placeholder="Property name" />
</div>
<div class="db-modal-field">
<label>Type</label>
<select x-model="newPropType">
<select id="db-prop-type-select">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="select">Select</option>
@@ -152,28 +60,8 @@
</select>
</div>
<div class="db-modal-actions">
<button class="db-btn-cancel" @click="propModalOpen=false">Cancel</button>
<button class="db-btn-create" @click="createProperty()">Create</button>
</div>
</div>
</div>
</div>
<!-- Page title rename (inline) -->
<div x-show="renameOpen" class="db-modal-overlay" @click.self="renameOpen=false" style="z-index:600">
<div class="db-modal" x-transition style="max-width:400px">
<div class="db-modal-header">
<h3>Rename page</h3>
<button @click="renameOpen=false">{{ fd_icon("x",14) }}</button>
</div>
<div class="db-modal-body">
<div class="db-modal-field">
<input x-model="renameTitle" type="text" placeholder="Page title"
@keydown.enter="commitRename()" x-ref="renameInput" />
</div>
<div class="db-modal-actions">
<button class="db-btn-cancel" @click="renameOpen=false">Cancel</button>
<button class="db-btn-create" @click="commitRename()">Save</button>
<button id="db-prop-cancel" class="db-btn-cancel">Cancel</button>
<button id="db-prop-create" class="db-btn-create">Create</button>
</div>
</div>
</div>
+316 -198
View File
@@ -1,57 +1,25 @@
<!-- ═══════════ Database Table Scripts ═══════════ -->
<!-- ═══════════ Database Table Scripts — Vanilla JS (no Alpine) ═══════════ -->
<script>
// Server-rendered initial data
const COLLECTION_ID = window.__DB_COLLECTION_ID || 0;
const PAGE_ID = window.__DB_PAGE_ID || 0;
(function() {
'use strict';
const COLLECTION_ID = window.__DB_COLLECTION_ID || 0;
const PAGE_ID = window.__DB_PAGE_ID || 0;
Alpine.data('databaseTable', () => ({
// State
pages: [],
properties: [],
addViewMenuOpen: false,
propModalOpen: false,
newPropName: '',
newPropType: 'text',
editingCell: null,
renameOpen: false,
renameTitle: '',
renamePageId: null,
dragIdx: null,
// ── State ──
const state = {
properties: [],
pages: [],
editingCell: null // {pageId, propId, input}
};
// ── Init ──
async init(el) {
{% if collection_data %}
// Preload from server
this.properties = {{ collection_data.properties | tojson }};
this.pages = ({{ collection_data.pages | tojson }}).map(p => ({...p, _hover: false, _selected: false}));
{% else %}
await this.loadData();
{% endif %}
console.log('[DB-Table] Init: collection_id=' + COLLECTION_ID, this.properties.length + ' props, ' + this.pages.length + ' pages');
},
// ── Icons helper (mirrors fd_icon on server side) ──
const propTypeIcons = {
title: 'Aa', text: 'Aa', number: '#', select: '☰',
multi_select: '☰', date: '📅', checkbox: '☑',
url: '🔗', email: '✉', phone: '📞'
};
async loadData() {
try {
const r = await fetch('/api/collections/' + COLLECTION_ID + '/table-data');
const d = await r.json();
this.properties = d.properties || [];
this.pages = (d.pages || []).map(p => ({...p, _hover: false, _selected: false}));
} catch(e) {
console.error('[DB-Table] loadData error:', e);
}
},
// ── Helpers ──
propTypeIcon(type) {
const icons = {
title: 'Aa', text: 'Aa', number: '#', select: '☰',
multi_select: '☰', date: '📅', checkbox: '☑',
url: '🔗', email: '✉', phone: '📞'
};
return icons[type] || 'Aa';
},
cellValue(cpage, prop) {
function cellValue(cpage, prop) {
if (!cpage.property_values_json) return '';
let pv;
try {
@@ -61,21 +29,221 @@ Alpine.data('databaseTable', () => ({
} catch(e) { return ''; }
const val = pv[String(prop.id)];
if (val === undefined || val === null) {
return prop.prop_type === 'title' ? cpage.title : '';
return prop.prop_type === 'title' ? (cpage.title || '') : '';
}
if (Array.isArray(val)) return val.join(', ');
return String(val);
},
}
emptyRows() {
// Aim for 5 visible rows total (1 New Page + data rows + empty)
const dataRows = this.pages.length;
const needed = Math.max(0, 4 - dataRows); // 4 data rows + 1 New = 5
return Array.from({length: needed}, (_, i) => i);
},
// ── Render ──
function renderTable() {
const thead = document.getElementById('db-thead');
const tbody = document.getElementById('db-tbody');
if (!thead || !tbody) return;
// ── Actions ──
async addNewPage() {
const props = state.properties;
const pages = state.pages;
// Header
let h = '<tr class="db-header-row">';
h += '<th class="db-th db-th-handle" style="width:32px"></th>';
for (const prop of props) {
h += '<th class="db-th db-th-property"><div class="db-th-content">';
h += '<span class="db-prop-type-icon">' + (propTypeIcons[prop.prop_type] || 'Aa') + '</span>';
h += '<span class="db-prop-name">' + escHtml(prop.name) + '</span>';
h += '</div></th>';
}
h += '<th class="db-th db-th-add-prop"><button id="db-add-prop-btn" class="db-add-prop-btn">+ Add property</button></th>';
h += '</tr>';
thead.innerHTML = h;
// Bind Add Property
const addBtn = document.getElementById('db-add-prop-btn');
if (addBtn) addBtn.onclick = showPropModal;
// Body
let b = '';
// New Page row
b += '<tr class="db-row db-row-new">';
b += '<td class="db-td db-td-handle"><span class="db-plus-icon">+</span></td>';
b += '<td class="db-td db-td-name" colspan="' + Math.max(1, props.length + 1) + '">';
b += '<button id="db-new-page-btn" class="db-new-page-btn">New page</button>';
b += '</td></tr>';
// Data rows
for (let i = 0; i < pages.length; i++) {
const cp = pages[i];
b += '<tr class="db-row" data-page-id="' + cp.id + '" data-idx="' + i + '">';
// Drag handle
b += '<td class="db-td db-td-handle">';
b += '<span class="db-drag-handle db-hover-only" style="visibility:hidden">⋮⋮</span>';
b += '</td>';
// Cells
for (const prop of props) {
const val = cellValue(cp, prop) || '\u2014';
const cellId = 'cell-' + cp.id + '-' + prop.id;
b += '<td class="db-td db-td-cell">';
b += '<div class="db-cell-content" id="' + cellId + '" data-page-id="' + cp.id + '" data-prop-id="' + prop.id + '">';
b += '<span class="db-cell-value">' + escHtml(val) + '</span>';
b += '</div></td>';
}
// Open button (hover only)
b += '<td class="db-td db-td-open db-hover-only" style="visibility:hidden">';
b += '<button class="db-open-btn db-open-page-btn" data-page-id="' + cp.id + '" title="Open">↗</button>';
b += '</td>';
b += '</tr>';
}
// Empty rows (fill to 5 total: 1 New Page + data + empty)
const emptyNeeded = Math.max(0, 4 - pages.length);
for (let i = 0; i < emptyNeeded; i++) {
b += '<tr class="db-row db-row-empty">';
b += '<td class="db-td db-td-handle"></td>';
for (let j = 0; j < props.length; j++) {
b += '<td class="db-td db-td-cell"><div class="db-cell-content"></div></td>';
}
b += '<td class="db-td db-td-open"></td>';
b += '</tr>';
}
tbody.innerHTML = b;
// Bind events after render
bindTableEvents();
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function bindTableEvents() {
const tbody = document.getElementById('db-tbody');
if (!tbody) return;
// New Page button
const newBtn = document.getElementById('db-new-page-btn');
if (newBtn) newBtn.onclick = addNewPage;
// Cell click → start editing
tbody.querySelectorAll('.db-cell-content').forEach(el => {
el.onclick = function(e) {
if (state.editingCell) return;
const pageId = parseInt(this.dataset.pageId);
const propId = parseInt(this.dataset.propId);
startEditCell(pageId, propId, this);
};
});
// Open buttons
tbody.querySelectorAll('.db-open-page-btn').forEach(btn => {
btn.onclick = function(e) {
e.stopPropagation();
const pageId = parseInt(this.dataset.pageId);
window.location.href = '/pages/' + pageId;
};
});
// Row hover effects
tbody.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'; });
};
});
}
// ── Cell Editing ──
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 span = cellEl.querySelector('.db-cell-value');
const currentVal = cellValue(cp, prop);
const input = document.createElement('input');
input.type = 'text';
input.className = 'db-cell-input';
input.value = currentVal;
input.style.cssText = 'width:100%;padding:3px 6px;border:1px solid #2383E2;border-radius:3px;font-size:14px;font-family:var(--font-sans);color:var(--text-primary);background:var(--bg-primary);outline:none;box-sizing:border-box;';
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;
const val = input.value;
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;
// Update local state
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; }
// Re-render cell
cellEl.classList.remove('db-cell-editing');
cellEl.innerHTML = '<span class="db-cell-value">' + escHtml(val || '\u2014') + '</span>';
cellEl.onclick = function(e) {
if (state.editingCell) return;
startEditCell(pageId, propId, cellEl);
};
// Persist to server
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
fetch('/db/pages/' + pageId + '/api', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf ? csrf[1] : ''
},
body: JSON.stringify({ title: cp.title, properties: pv })
}).catch(function(e) { console.error('[DB] commitEditCell error:', e); });
}
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');
cellEl.innerHTML = '<span class="db-cell-value">' + escHtml(val || '\u2014') + '</span>';
cellEl.onclick = function(e) {
if (state.editingCell) return;
startEditCell(pageId, propId, cellEl);
};
}
// ── Add New Page ──
async function addNewPage() {
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
try {
const r = await fetch('/api/collections/' + COLLECTION_ID + '/pages', {
@@ -84,26 +252,40 @@ Alpine.data('databaseTable', () => ({
'Content-Type': 'application/json',
'X-CSRF-Token': csrf ? csrf[1] : ''
},
body: JSON.stringify({title: 'New page'})
body: JSON.stringify({ title: 'New page' })
});
const d = await r.json();
if (d.id) {
this.pages.push({...d, _hover: false, _selected: false});
console.log('[DB-Table] New page created:', d.id);
state.pages.push({
id: d.id, title: d.title, position: d.position,
icon: 'file', collection_id: COLLECTION_ID,
property_values_json: {}
});
renderTable();
console.log('[DB] New page created:', d.id);
}
} catch(e) {
console.error('[DB-Table] addNewPage error:', e);
console.error('[DB] addNewPage error:', e);
}
},
}
async addProperty() {
this.propModalOpen = true;
this.newPropName = '';
this.newPropType = 'text';
},
// ── Property Modal ──
function showPropModal() {
document.getElementById('db-prop-modal').style.display = 'flex';
document.getElementById('db-prop-name-input').value = '';
document.getElementById('db-prop-name-input').focus();
}
function hidePropModal() {
document.getElementById('db-prop-modal').style.display = 'none';
}
async function createProperty() {
const nameInput = document.getElementById('db-prop-name-input');
const typeSelect = document.getElementById('db-prop-type-select');
const name = nameInput.value.trim();
if (!name) return;
async createProperty() {
if (!this.newPropName.trim()) return;
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
try {
const r = await fetch('/db/' + COLLECTION_ID + '/properties/api', {
@@ -112,137 +294,73 @@ Alpine.data('databaseTable', () => ({
'Content-Type': 'application/json',
'X-CSRF-Token': csrf ? csrf[1] : ''
},
body: JSON.stringify({
name: this.newPropName.trim(),
prop_type: this.newPropType
})
body: JSON.stringify({ name: name, prop_type: typeSelect.value })
});
const d = await r.json();
if (d.id) {
this.properties.push(d);
this.propModalOpen = false;
console.log('[DB-Table] Property created:', d.name);
state.properties.push(d);
renderTable();
hidePropModal();
console.log('[DB] Property created:', d.name);
}
} catch(e) {
console.error('[DB-Table] createProperty error:', e);
console.error('[DB] createProperty error:', e);
}
},
// ── Cell editing ──
startEditCell(pageId, propId, event) {
// Click on cell starts editing
this.editingCell = {pageId, propId};
this.$nextTick(() => {
const input = this.$refs.cellInput;
if (input) {
if (Array.isArray(input)) input[0]?.focus();
else input.focus();
}
});
},
async commitEditCell(cpage, prop) {
if (!this.editingCell) return;
const input = this.$refs.cellInput;
const val = Array.isArray(input) ? input[0]?.value : input?.value;
this.editingCell = null;
if (val === undefined) return;
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
try {
// Update local state first
let pv;
try {
pv = typeof cpage.property_values_json === 'string'
? JSON.parse(cpage.property_values_json)
: cpage.property_values_json;
} catch(e) { pv = {}; }
pv[String(prop.id)] = val;
cpage.property_values_json = pv;
// If it's the title property, also update the title field
if (prop.prop_type === 'title') {
cpage.title = val;
}
// Persist to server
await fetch('/db/pages/' + cpage.id + '/api', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf ? csrf[1] : ''
},
body: JSON.stringify({
title: cpage.title,
properties: pv
})
});
} catch(e) {
console.error('[DB-Table] commitEditCell error:', e);
}
},
cancelEditCell() {
this.editingCell = null;
},
// ── Open page ──
openPage(cpageId) {
// Open the collection page as a regular page
window.location.href = '/pages/' + cpageId;
},
// ── Drag (placeholder) ──
startDrag(idx, event) {
this.dragIdx = idx;
console.log('[DB-Table] Drag start:', idx);
},
// ── Rename ──
startRename(cpage) {
this.renamePageId = cpage.id;
this.renameTitle = cpage.title;
this.renameOpen = true;
this.$nextTick(() => {
const input = this.$refs.renameInput;
if (input) {
if (Array.isArray(input)) input[0]?.focus();
else input.focus();
}
});
},
async commitRename() {
if (!this.renamePageId || !this.renameTitle.trim()) {
this.renameOpen = false;
return;
}
const cpage = this.pages.find(p => p.id === this.renamePageId);
if (cpage) cpage.title = this.renameTitle.trim();
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
try {
await fetch('/db/pages/' + this.renamePageId + '/api', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf ? csrf[1] : ''
},
body: JSON.stringify({
title: this.renameTitle.trim()
})
});
} catch(e) {
console.error('[DB-Table] rename error:', e);
}
this.renameOpen = false;
this.renamePageId = null;
},
// ── View switching (placeholder) ──
switchView(type) {
console.log('[DB-Table] Switch view to:', type);
}
}));
// ── View Dropdown ──
function bindUI() {
// Add view dropdown toggle
document.getElementById('db-view-add-btn').onclick = function(e) {
e.stopPropagation();
const dd = document.getElementById('db-add-view-dropdown');
dd.style.display = dd.style.display === 'none' ? 'block' : 'none';
};
document.addEventListener('click', function() {
document.getElementById('db-add-view-dropdown').style.display = 'none';
});
// Property modal
document.getElementById('db-prop-modal-close').onclick = hidePropModal;
document.getElementById('db-prop-cancel').onclick = hidePropModal;
document.getElementById('db-prop-create').onclick = createProperty;
document.getElementById('db-prop-modal').onclick = function(e) {
if (e.target === this) hidePropModal();
};
document.getElementById('db-prop-name-input').onkeydown = function(e) {
if (e.key === 'Enter') createProperty();
};
// New button (top right)
document.getElementById('db-new-btn').onclick = addNewPage;
}
// ── Load Data ──
async function loadData() {
{% if collection_data %}
// Preloaded from server
state.properties = {{ collection_data.properties | tojson }};
state.pages = {{ collection_data.pages | tojson }};
{% else %}
try {
const r = await fetch('/api/collections/' + COLLECTION_ID + '/table-data');
const d = await r.json();
state.properties = d.properties || [];
state.pages = d.pages || [];
} catch(e) {
console.error('[DB] loadData error:', e);
}
{% endif %}
console.log('[DB-Table] Init: collection_id=' + COLLECTION_ID, state.properties.length + ' props, ' + state.pages.length + ' pages');
renderTable();
bindUI();
}
// ── Start ──
if (COLLECTION_ID > 0) {
loadData();
} else {
console.warn('[DB-Table] No collection_id — cannot load database table');
}
})();
</script>