feat(db): v5.3.0 inline databases, predefined templates & property validation
- Slash command /database (DATA group) -> template picker -> inline DB embed block rendered by FlowDeckDB. 'Get Started > Database' uses it too. - 6 seeded database templates (CRM, Project tracker, Task list, Content calendar, Meeting notes, Reading list) with icon + schema; new service app/services/db_templates.py (materialize_properties, create_from_template). - POST /db/api and /db/inline/api accept 'template' and materialize collection_properties. database_templates gets an icon column (migration v4). - Property validation: collection_properties.validation_json (migration v4); validate_property_rule() in property_types (required/unique/min/max/ min_length/max_length); enforced in create/update page API (400 + message, unique excludes current row); property API accepts 'validation'. - UI: add-property modal exposes Required/Unique/Min/Max; cell edit shows validation errors (red outline + toast) and reverts. - VERSION + app.main -> 5.1.0; CHANGELOG + ROADMAP updated. - +9 tests (tests/test_db_advanced.py). 271 passed.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<!-- ═══════════ Database Table Scripts — Vanilla JS ═══════════ -->
|
||||
<style>.db-cell-invalid{outline:2px solid #e5484d;outline-offset:-1px;border-radius:4px}</style>
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
@@ -206,7 +207,31 @@
|
||||
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})}).catch(function(e){});
|
||||
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() {
|
||||
@@ -246,6 +271,14 @@
|
||||
'<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);
|
||||
@@ -261,10 +294,17 @@
|
||||
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})
|
||||
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(); }
|
||||
|
||||
Reference in New Issue
Block a user