feat(core): v5.0.0 command palette + FTS5 search, v5.2.0 versioned migrations
- Command palette Ctrl+K/Ctrl+P (base.html): universal search, fuzzy highlight, keyboard nav, quick actions. openQuickFind now opens it. - GET /api/search endpoint + search service: unified pages + databases search, FTS5 with LIKE fallback, trash excluded, user-scoped. - app/migrations.py: lightweight versioned migration runner (schema_version table); baseline schema = v1, new steps applied in order. - Migration v2: missing indexes (users.email, user_oauth_tokens, etc). - Migration v3: FTS5 pages_fts + sync triggers + backfill. - VERSION + app.main -> 5.0.0; CHANGELOG + ROADMAP updated. - +8 tests (tests/test_search_migrations.py). 259 passed (3 pre-existing PDF/weasyprint env failures unrelated).
This commit is contained in:
+181
-2
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FlowDeck — {% block title_prefix %}Home{% endblock %}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="stylesheet" href="/static/css/app.css?v=4.8.1">
|
||||
<link rel="stylesheet" href="/static/css/app.css?v=5.0.0">
|
||||
<link rel="stylesheet" href="/static/css/katex.min.css?v=0.16.11">
|
||||
<style>
|
||||
/* ── Mobile responsive (v4.0.2) ── */
|
||||
@@ -1236,7 +1236,7 @@
|
||||
this.sidebarCollapsed = true;
|
||||
window.location.href = url;
|
||||
},
|
||||
openQuickFind() { this.toast('Quick Find — Ctrl+K', 'info'); },
|
||||
openQuickFind() { if (window.FlowDeckPalette) window.FlowDeckPalette.open(); },
|
||||
|
||||
// ── Tree state ──
|
||||
expandedNodes: {},
|
||||
@@ -1859,5 +1859,184 @@
|
||||
|
||||
{% include 'agent_panel.html' %}
|
||||
|
||||
<!-- ═══════════════════ COMMAND PALETTE (v5.0.0) — Ctrl+K / Ctrl+P ═══════════════════ -->
|
||||
<style>
|
||||
.cmd-palette-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2100;display:flex;justify-content:center;padding-top:14vh;opacity:0;visibility:hidden;transition:opacity .12s ease,visibility .12s ease}
|
||||
.cmd-palette-overlay.open{opacity:1;visibility:visible}
|
||||
.cmd-palette{width:600px;max-width:92vw;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-secondary,#232323);border:1px solid var(--border,rgba(255,255,255,.08));border-radius:12px;box-shadow:0 24px 70px rgba(0,0,0,.6);overflow:hidden;transform:translateY(8px);transition:transform .15s ease}
|
||||
.cmd-palette-overlay.open .cmd-palette{transform:translateY(0)}
|
||||
.cmd-palette-input{width:100%;box-sizing:border-box;padding:16px 18px;background:transparent;border:none;outline:none;color:var(--text,#fff);font-size:16px}
|
||||
.cmd-palette-input::placeholder{color:var(--text-dim,rgba(255,255,255,.4))}
|
||||
.cmd-palette-list{overflow-y:auto;border-top:1px solid var(--border,rgba(255,255,255,.06))}
|
||||
.cmd-palette-group{display:flex;align-items:center;gap:8px;padding:10px 18px 4px;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-dim,rgba(255,255,255,.4))}
|
||||
.cmd-palette-item{display:flex;align-items:center;gap:10px;padding:9px 18px;cursor:pointer;font-size:14px;color:var(--text,#fff)}
|
||||
.cmd-palette-item .cpi-icon{width:20px;text-align:center;flex:none;color:var(--text-dim,rgba(255,255,255,.5))}
|
||||
.cmd-palette-item .cpi-main{flex:1;min-width:0}
|
||||
.cmd-palette-item .cpi-title{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cmd-palette-item .cpi-sub{font-size:12px;color:var(--text-dim,rgba(255,255,255,.45));white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cmd-palette-item .cpi-sub mark{background:none;color:var(--accent,#2383E2);font-weight:600}
|
||||
.cmd-palette-item .cpi-title mark{background:none;color:var(--accent,#2383E2);font-weight:600}
|
||||
.cmd-palette-item .cpi-key{margin-left:auto;flex:none;font-size:11px;color:var(--text-dim,rgba(255,255,255,.35));font-family:monospace}
|
||||
.cmd-palette-item.active{background:var(--bg-hover,#2e2e2e)}
|
||||
.cmd-palette-empty{padding:22px 18px;color:var(--text-dim,rgba(255,255,255,.4));font-size:13px;text-align:center}
|
||||
.cmd-palette-footer{display:flex;align-items:center;gap:14px;padding:8px 16px;border-top:1px solid var(--border,rgba(255,255,255,.06));font-size:11px;color:var(--text-dim,rgba(255,255,255,.4))}
|
||||
.cmd-palette-footer .cpf-kbd{display:inline-flex;align-items:center;gap:3px}
|
||||
.cmd-palette-footer b{color:var(--text,rgba(255,255,255,.8));font-weight:600}
|
||||
</style>
|
||||
<div id="fd-command-palette" class="cmd-palette-overlay" onclick="if(event.target===this)window.FlowDeckPalette&&window.FlowDeckPalette.close()">
|
||||
<div class="cmd-palette" role="dialog" aria-label="Quick find">
|
||||
<input id="fd-cp-input" class="cmd-palette-input" type="text"
|
||||
placeholder="Search pages, databases, or type a command…"
|
||||
autocomplete="off" spellcheck="false">
|
||||
<div id="fd-cp-list" class="cmd-palette-list"></div>
|
||||
<div class="cmd-palette-footer">
|
||||
<span class="cpf-kbd"><b>↑</b> / <b>↓</b> navigate</span>
|
||||
<span class="cpf-kbd"><b>↵</b> open</span>
|
||||
<span class="cpf-kbd"><b>esc</b> close</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var PALETTE_ACTIONS = [
|
||||
{ id:'new-page', icon:'📄', title:'New page', sub:'Create a new page in the current workspace', key:'Ctrl N', run:function(){ return window.FlowDeck && window.FlowDeck.createPage ? (window.FlowDeck.createPage(), true) : false; } },
|
||||
{ id:'goto-library', icon:'📚', title:'Go to Library', sub:'Recents · Favorites · Shared · Published', key:'', run:function(){ window.location.href='/library'; return true; } },
|
||||
{ id:'goto-mytasks', icon:'✅', title:'My Tasks', sub:'Aggregated tasks across databases', key:'', run:function(){ window.location.href='/my-tasks'; return true; } },
|
||||
{ id:'goto-trash', icon:'🗑️', title:'Trash', sub:'Recently deleted pages', key:'', run:function(){ window.location.href='/trash'; return true; } },
|
||||
{ id:'goto-settings', icon:'⚙️', title:'Settings', sub:'Account, integrations, notifications', key:'', run:function(){ window.location.href='/accounts/settings'; return true; } },
|
||||
{ id:'goto-help', icon:'❓', title:'Help & shortcuts', sub:'Keyboard shortcuts and guides', key:'', run:function(){ window.location.href='/help'; return true; } }
|
||||
];
|
||||
|
||||
var overlay, input, list;
|
||||
var state = { open:false, items:[], index:0, query:'', timer:null, actions:false };
|
||||
|
||||
function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); }
|
||||
function highlight(text, q){
|
||||
text = esc(text);
|
||||
var terms = (q||'').split(/\s+/).filter(function(t){return t;}).map(function(t){return t.toLowerCase();});
|
||||
if(!terms.length) return text;
|
||||
var lower = text.toLowerCase();
|
||||
var out='', i=0;
|
||||
for(var k=0;k<terms.length;k++){
|
||||
var term=terms[k];
|
||||
if(!term) continue;
|
||||
var idx = lower.indexOf(term, i);
|
||||
if(idx>=0){
|
||||
out += text.slice(i, idx);
|
||||
out += '<mark>'+text.slice(idx, idx+term.length)+'</mark>';
|
||||
i = idx + term.length;
|
||||
}
|
||||
}
|
||||
out += text.slice(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function render(){
|
||||
if(!state.open) return;
|
||||
if(!state.items.length){
|
||||
list.innerHTML = '<div class="cmd-palette-empty">No results for “'+esc(state.query)+'”</div>';
|
||||
return;
|
||||
}
|
||||
var groups = [];
|
||||
var current = null;
|
||||
state.items.forEach(function(item, idx){
|
||||
var gkey = item.group || (item.type==='collection'?'Databases':(item.type==='page'?'Pages':'Actions'));
|
||||
if(gkey!==current){ current=gkey; groups.push({gkey:gkey, items:[]}); }
|
||||
groups[groups.length-1].items.push({item:item, idx:idx});
|
||||
});
|
||||
var html='';
|
||||
groups.forEach(function(g){
|
||||
html += '<div class="cmd-palette-group">'+esc(g.gkey)+'</div>';
|
||||
g.items.forEach(function(gi){
|
||||
var it=gi.item;
|
||||
var cls = gi.idx===state.index ? 'cmd-palette-item active' : 'cmd-palette-item';
|
||||
var icon = it.icon || (it.type==='page'?'📄':'📋');
|
||||
var sub = it.subtitle || it.excerpt || '';
|
||||
var key = it.key || '';
|
||||
html += '<div class="'+cls+'" data-idx="'+gi.idx+'" onmouseenter="window.FlowDeckPalette.setIndex('+gi.idx+')" onclick="window.FlowDeckPalette.choose('+gi.idx+')">'
|
||||
+ '<span class="cpi-icon">'+esc(icon)+'</span>'
|
||||
+ '<span class="cpi-main"><span class="cpi-title">'+highlight(it.title, state.query)+'</span>'
|
||||
+ (sub ? '<div class="cpi-sub">'+highlight(sub, state.query)+'</div>' : '')
|
||||
+ '</span>'
|
||||
+ (key ? '<span class="cpi-key">'+esc(key)+'</span>' : '')
|
||||
+ '</div>';
|
||||
});
|
||||
});
|
||||
list.innerHTML = html;
|
||||
var active = list.querySelector('.cmd-palette-item.active');
|
||||
if(active) active.scrollIntoView({block:'nearest'});
|
||||
}
|
||||
|
||||
function runSearch(){
|
||||
var q = input.value.trim();
|
||||
state.query = q;
|
||||
state.index = 0;
|
||||
if(!q){
|
||||
state.items = PALETTE_ACTIONS.map(function(a){ return Object.assign({group:'Actions', type:'action'}, a); });
|
||||
state.actions = true;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
state.actions = false;
|
||||
fetch('/api/search?q='+encodeURIComponent(q), {headers:{'X-CSRF-Token': document.body.getAttribute('hx-headers') ? (JSON.parse(document.body.getAttribute('hx-headers'))['X-CSRF-Token']||'') : ''}})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(data){
|
||||
if(input.value.trim()!==q) return; // stale
|
||||
var pages=(data.pages||[]).slice(0,8), cols=(data.collections||[]).slice(0,6);
|
||||
var items = pages.map(function(p){ return Object.assign({group:'Pages'}, p); })
|
||||
.concat(cols.map(function(c){ return Object.assign({group:'Databases'}, c); }))
|
||||
.concat(PALETTE_ACTIONS.map(function(a){ return Object.assign({group:'Actions', type:'action'}, a); }));
|
||||
state.items = items;
|
||||
render();
|
||||
})
|
||||
.catch(function(){ state.items=[]; render(); });
|
||||
}
|
||||
|
||||
function open(){
|
||||
if(state.open) return;
|
||||
state.open=true; overlay.classList.add('open');
|
||||
document.body.style.overflow='hidden';
|
||||
setTimeout(function(){ input.focus(); }, 30);
|
||||
runSearch();
|
||||
}
|
||||
function close(){
|
||||
if(!state.open) return;
|
||||
state.open=false; overlay.classList.remove('open');
|
||||
document.body.style.overflow='';
|
||||
input.blur();
|
||||
}
|
||||
function toggle(){ state.open ? close() : open(); }
|
||||
function setIndex(i){ if(i>=0 && i<state.items.length){ state.index=i; render(); } }
|
||||
function choose(i){
|
||||
var it = state.items[i]; if(!it) return;
|
||||
if(it.run){ var ok = it.run(); if(ok) close(); return; }
|
||||
if(it.url){ window.location.href=it.url; close(); return; }
|
||||
}
|
||||
|
||||
function onKey(e){
|
||||
var tag=(document.activeElement&&document.activeElement.tagName)||'';
|
||||
var mod = e.ctrlKey||e.metaKey;
|
||||
if(mod && (e.key==='k'||e.key==='K'||e.key==='p'||e.key==='P')){
|
||||
e.preventDefault(); toggle(); return;
|
||||
}
|
||||
if(!state.open) return;
|
||||
if(e.key==='Escape'){ e.preventDefault(); close(); return; }
|
||||
if(e.key==='ArrowDown'){ e.preventDefault(); setIndex(Math.min(state.index+1, state.items.length-1)); }
|
||||
else if(e.key==='ArrowUp'){ e.preventDefault(); setIndex(Math.max(state.index-1, 0)); }
|
||||
else if(e.key==='Enter'){ e.preventDefault(); choose(state.index); }
|
||||
}
|
||||
|
||||
window.FlowDeckPalette = { open:open, close:close, toggle:toggle, setIndex:setIndex, choose:choose, getState:function(){return state;} };
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
overlay = document.getElementById('fd-command-palette');
|
||||
input = document.getElementById('fd-cp-input');
|
||||
list = document.getElementById('fd-cp-list');
|
||||
if(!overlay) return;
|
||||
input.addEventListener('input', function(){ clearTimeout(state.timer); state.timer=setTimeout(runSearch, 150); });
|
||||
document.addEventListener('keydown', onKey);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user