feat: Database button in page editor — creates real inline collections
- 'Get started with' Database/Form buttons now POST to /db/inline/api - Creates real collections linked to the page via parent_page_id - Inline DBs rendered as embed blocks with links to collection views - Persisted in page blocks JSON (embed_type='inline_dbs') - Templates button opens the 'More' dropdown with all view types - 175 tests pass
This commit is contained in:
@@ -457,19 +457,19 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
</button>
|
||||
<button
|
||||
class="gs-pill"
|
||||
@click="replaceBlock(0, 'paragraph');blocks[0].content='Database';render()"
|
||||
@click="createDatabase()"
|
||||
>
|
||||
<span class="gs-icon">{{ fd_icon("folder",14) }}</span> Database
|
||||
</button>
|
||||
<button
|
||||
class="gs-pill"
|
||||
@click="replaceBlock(0, 'paragraph');blocks[0].content='Form';render()"
|
||||
@click="createForm()"
|
||||
>
|
||||
<span class="gs-icon">{{ fd_icon("copy",14) }}</span> Form
|
||||
</button>
|
||||
<button
|
||||
class="gs-pill"
|
||||
@click="replaceBlock(0, 'paragraph');blocks[0].content='Templates';render()"
|
||||
@click="gsMoreOpen = !gsMoreOpen"
|
||||
>
|
||||
<span class="gs-icon">{{ fd_icon("file",14) }}</span> Templates
|
||||
</button>
|
||||
@@ -575,6 +575,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
if(b.type==='divider')return `<div class="block-wrapper"><hr class="block-divider"></div>`;
|
||||
if(b.type==='image')return `<div class="block-wrapper"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'"></div></div>`;
|
||||
if(b.type==='embed'){
|
||||
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper"><div class="block-content block-inline-dbs"><div style="padding:20px;text-align:center;color:var(--text-secondary);">📊 Inline Databases</div><div class="inline-dbs-grid">${(b.dbs||[]).map(db=>`<a href="/db/${db.id}/view/${db.view_type||'table'}" class="inline-db-card" target="_blank" style="display:block;padding:12px 16px;background:var(--bg-secondary);border-radius:8px;margin:8px 0;text-decoration:none;color:var(--text-primary);border:1px solid var(--border);"><span style="font-weight:500">📋 ${db.name}</span><span style="float:right;font-size:12px;color:var(--text-tertiary);">${db.view_type==='form'?'📝 Form':'📊 Table'} →</span></a>`).join('')}</div></div></div>`;
|
||||
if(b.embed_type==='pdf')return `<div class="block-wrapper"><div class="block-content block-embed" style="padding:0;"><iframe src="${b.src||''}" style="width:100%;height:80vh;border:none;border-radius:8px;" onerror="this.style.display='none';this.nextElementSibling.style.display='block';"></iframe><div style="display:none;text-align:center;padding:32px;color:var(--text-secondary);"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p style="margin-top:8px;">Failed to load PDF</p><a href="${b.src||''}" target="_blank" style="color:var(--accent);">Open in new tab →</a></div></div></div>`;
|
||||
if(b.embed_type==='download'){
|
||||
var sz=b.file_size||0;var szStr=sz<1024?sz+' B':sz<1048576?(sz/1024).toFixed(1)+' KB':(sz/1048576).toFixed(1)+' MB';
|
||||
@@ -636,6 +637,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);this.blocks.forEach(b=>{if(!b.id)b.id=genId()});}catch(e){this.blocks=[];}}
|
||||
else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}}
|
||||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||||
this.loadInlineDBs();
|
||||
window.E=this;SM.init();this.render();
|
||||
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
|
||||
ct.addEventListener('keydown',e=>this.onKd(e));
|
||||
@@ -841,6 +843,71 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
},
|
||||
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.5.0+: Get Started actions ──
|
||||
inlineDBs:[],
|
||||
async createDatabase(){
|
||||
const name=prompt('Database name:','New Database');
|
||||
if(!name||!name.trim())return;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
try{
|
||||
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.trim(),parent_page_id:this.pid})
|
||||
});
|
||||
const d=await r.json();
|
||||
if(d.id){
|
||||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'table'});
|
||||
this.showToast('Database created — '+d.name);
|
||||
this.saveDBsToPage();
|
||||
this.renderInlineDBs();
|
||||
}else{
|
||||
this.showToast('Failed to create database');
|
||||
}
|
||||
}catch(e){this.showToast('Error: '+e.message);}
|
||||
},
|
||||
async createForm(){
|
||||
const name=prompt('Form name:','New Form');
|
||||
if(!name||!name.trim())return;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
try{
|
||||
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.trim(),parent_page_id:this.pid})
|
||||
});
|
||||
const d=await r.json();
|
||||
if(d.id){
|
||||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'form'});
|
||||
this.showToast('Form created — '+d.name);
|
||||
this.saveDBsToPage();
|
||||
this.renderInlineDBs();
|
||||
}else{
|
||||
this.showToast('Failed to create form');
|
||||
}
|
||||
}catch(e){this.showToast('Error: '+e.message);}
|
||||
},
|
||||
saveDBsToPage(){
|
||||
// Store inline DB refs in the page's blocks as a special embed block
|
||||
if(this.inlineDBs.length===0)return;
|
||||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||||
if(existing.length===0){
|
||||
this.blocks.push(this.mkB('embed','',{embed_type:'inline_dbs',dbs:this.inlineDBs}));
|
||||
}else{
|
||||
existing.forEach(b=>{b.dbs=this.inlineDBs;});
|
||||
}
|
||||
this.render();
|
||||
this.dirty=true;this.autoSave();
|
||||
},
|
||||
renderInlineDBs(){
|
||||
// Re-render with inline DB blocks
|
||||
this.render();
|
||||
},
|
||||
loadInlineDBs(){
|
||||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||||
if(existing.length>0){
|
||||
this.inlineDBs=existing[0].dbs||[];
|
||||
}
|
||||
},
|
||||
|
||||
onKd(e){
|
||||
const ab=this.getActiveBlock();if(!ab)return;
|
||||
const{el,bid,idx}=ab;const block=this.blocks[idx];if(!block)return;
|
||||
|
||||
Reference in New Issue
Block a user