feat: Notion-style block editor v1 + Move to Trash + fixes
FlowDeck CI / test (push) Failing after 8s
FlowDeck CI / docker (push) Has been skipped

- Block editor: 13 block types with contentEditable + vanilla JS event delegation
- Slash menu /: 14 commands, 100% vanilla JS (zero Alpine conflicts)
- Keyboard: Enter=new block, Shift+Enter=soft break, arrows navigate blocks
- Drag & drop: SortableJS with 6-dot handles on block-wrapper
- Move to Trash: DELETE /board/api/pages/{id} + instant sidebar cleanup
- Placeholders: 'New page' + 'Type / for commands' in gray (CSS :empty::before)
- Redirect after delete stays in project context via workspaceKey
- Fixes: title auto-save, cursor positioning, x-for keys, Sortable guard
This commit is contained in:
2026-07-09 11:29:36 -04:00
parent 06cdec52fa
commit d4cd45c6cb
5 changed files with 1009 additions and 217 deletions
+26 -4
View File
@@ -389,6 +389,7 @@
// ── Drag & drop tree ──
treeSortables: [],
initTreeSortable() {
if (typeof Sortable === 'undefined') return;
// Initialize SortableJS on every .sidebar-items list
const self = this;
document.querySelectorAll('.sidebar-items').forEach(function(el) {
@@ -475,8 +476,29 @@
alert(`Moved to ${section}`);
break;
case 'delete':
if (!confirm(`Delete "${pageName}"?`)) break;
alert('Delete not yet implemented (pages go to Trash in DB)');
if (!confirm(`Move "${pageName}" to trash?`)) break;
const delNum = pageId.replace('page/', '');
fetch(`/board/api/pages/${delNum}`, {
method: 'DELETE', headers: { 'X-CSRF-Token': csrf }
})
.then(r => { if (!r.ok) throw new Error('Delete failed'); return r.json(); })
.then(data => {
// Remove from ALL sidebar sections (Recents, Private, etc.)
const items = document.querySelectorAll(`.sidebar-item[data-page-id="${pageId}"]`);
items.forEach(item => {
const next = item.nextElementSibling;
if (next && next.classList.contains('tree-children-wrapper')) {
next.remove();
}
item.remove();
});
// Redirect if viewing deleted page — stay in project context
if (window.location.pathname.includes(`/pages/${delNum}`)) {
const wk = this.workspaceKey;
window.location.href = wk && wk.includes('/') ? `/board/${wk}` : '/';
}
})
.catch(e => alert('Delete failed: ' + e.message));
break;
case 'openTab':
window.open('/' + pageId, '_blank');
@@ -501,7 +523,7 @@
},
newPage(section) {
const project = this.workspaceKey || '';
fetch(`/board/api/pages?title=New%20page&section=${encodeURIComponent(section)}&project=${encodeURIComponent(project)}`, {
fetch(`/board/api/pages?section=${encodeURIComponent(section)}&project=${encodeURIComponent(project)}`, {
method: 'POST',
headers: { 'X-CSRF-Token': this.getCsrfToken() }
})
@@ -512,7 +534,7 @@
newSubPage(parentId) {
const project = this.workspaceKey || '';
const pid = parentId.replace('page/', '');
fetch(`/board/api/pages?title=New%20sub-page&section=Private&project=${encodeURIComponent(project)}&parent_id=${pid}`, {
fetch(`/board/api/pages?section=Private&project=${encodeURIComponent(project)}&parent_id=${pid}`, {
method: 'POST',
headers: { 'X-CSRF-Token': this.getCsrfToken() }
})