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:
@@ -10,7 +10,8 @@
|
||||
{id:'callout',name:'Callout',icon:'lightbulb'},{id:'table_of_contents',name:'Table of contents',icon:'☰'},{id:'math',name:'Math equation',icon:'∑'},
|
||||
{id:'columns',name:'Column list',icon:'▮'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'},
|
||||
{id:'table',name:'Table',icon:'⊞'},
|
||||
]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'image'}]}];
|
||||
]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'image'}]},
|
||||
{name:'DATA',items:[{id:'database',name:'Database / Inline database',icon:'📊'}]}];
|
||||
|
||||
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
|
||||
function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;}
|
||||
@@ -871,26 +872,48 @@
|
||||
},
|
||||
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});},
|
||||
|
||||
// ── v4.6.0: Get Started actions ──
|
||||
// ── v4.6.0 / v5.3.0: Get Started actions — Database (template picker) ──
|
||||
dbTplOpen:false, dbTplLoading:false, dbTemplates:[], dbTplTargetIdx:-1,
|
||||
async createDatabase(){
|
||||
this.openDbTemplatePicker(-1);
|
||||
},
|
||||
async openDbTemplatePicker(targetIdx){
|
||||
this.gsMoreOpen=false;
|
||||
this.dbTplOpen=true; this.dbTplTargetIdx=targetIdx; this.dbTplLoading=true;
|
||||
var self=this;
|
||||
try{
|
||||
const r=await fetch('/workspace/templates/database');
|
||||
const d=await r.json();
|
||||
self.dbTemplates=d.templates||[];
|
||||
}catch(e){ self.dbTemplates=[]; }
|
||||
self.dbTplLoading=false;
|
||||
self.$nextTick(()=>{ const f=document.getElementById('fd-dbtpl'); if(f) setTimeout(()=>{f.style.opacity='1';},10); });
|
||||
},
|
||||
closeDbTemplatePicker(){ this.dbTplOpen=false; },
|
||||
async createDbFromTemplate(tplName){
|
||||
const self=this;
|
||||
var name = tplName ? (tplName+' — '+new Date().toLocaleDateString('fr-FR')) : 'Database';
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
try{
|
||||
const name='Database';
|
||||
// Create collection via API
|
||||
const r=await fetch('/db/api',{
|
||||
const body={name:name, parent_page_id:this.pid};
|
||||
if(tplName) body.template=tplName;
|
||||
const r=await fetch('/db/inline/api',{
|
||||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
|
||||
body:JSON.stringify({name:name})
|
||||
body:JSON.stringify(body)
|
||||
});
|
||||
const d=await r.json();
|
||||
if(d.id){
|
||||
// Add inline database embed block to the page
|
||||
this.blocks.push(this.mkB('embed','',{embed_type:'collection',collection_id:d.id}));
|
||||
this.render(); this.dirty=true; this.autoSave();
|
||||
this.showToast('Database created');
|
||||
const nb=this.mkB('embed','',{embed_type:'collection',collection_id:d.id});
|
||||
this.sync();
|
||||
const idx=this.dbTplTargetIdx>=0?this.dbTplTargetIdx:(this.blocks.length);
|
||||
this.blocks.splice(idx,0,nb);
|
||||
this.dbTplOpen=false;
|
||||
this.dirty=true; this.autoSave(); this.render();
|
||||
this.showToast(tplName?('Database created from template — '+tplName):'Database created');
|
||||
}else{
|
||||
this.showToast('Failed to create database');
|
||||
this.showToast(d.detail||'Failed to create database');
|
||||
}
|
||||
}catch(e){this.showToast('Error: '+e.message);}
|
||||
}catch(e){ this.showToast('Error: '+e.message); }
|
||||
},
|
||||
async createForm(){
|
||||
const name=prompt('Form name:','New Form');
|
||||
@@ -979,6 +1002,7 @@
|
||||
},
|
||||
applySlash(id){
|
||||
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
|
||||
if(id==='database'){ this.openDbTemplatePicker(idx); return; }
|
||||
const block=this.blocks[idx];this.sync();block.content='';
|
||||
if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');}
|
||||
else{block.type=id;if(id==='toggle'){block.expanded=true;block.children=[];}if(id==='to_do')block.checked=false;if(id==='columns')block.children=[{id:genId(),type:'paragraph',content:''}];if(id==='code')block.language='Plain Text';if(id==='table'){block.rows=_tblNew(3,3);block.content='';}}
|
||||
|
||||
Reference in New Issue
Block a user