fix: Library page — hover effects, workspace filter, tree, rename
1. Hover effects fixed: converted table to div-based flexbox layout (tr/td don't support display:flex). .lib-row now properly shows drag handle, checkbox, and OPEN button on hover. 2. Recents filter by workspace: /api/library/recents now accepts workspace_id parameter. Template passes active_workspace_id from sidebar context to API calls. 3. Tree hierarchy: API now enriches items with has_children via _enrich_children() batch query — shows expand chevrons for pages with sub-pages. 4. Rename on click: single click on title starts inline rename (was opening page). OPEN button still opens side peek. 5. Code cleanup: extracted _enrich_children() helper to eliminate duplicate code across 6 endpoints.
This commit is contained in:
+60
-54
@@ -11,6 +11,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div x-data="libraryPage()" x-init="init()" @keydown.escape="closePeek()">
|
||||
<div id="lib-workspace-id" style="display:none;">{{ active_workspace_id or 0 }}</div>
|
||||
|
||||
<style>
|
||||
/* ── Library layout ── */
|
||||
@@ -32,22 +33,23 @@
|
||||
.lib-select-bar button{background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:4px 8px;border-radius:4px;font-size:13px;display:flex;align-items:center;gap:4px;transition:all 0.15s;}
|
||||
.lib-select-bar button:hover{background:var(--bg-hover);color:var(--text-primary);}
|
||||
|
||||
/* ── Table ── */
|
||||
.lib-table{width:100%;border-collapse:collapse;table-layout:fixed;}
|
||||
.lib-table th{text-align:left;padding:6px 8px;font-size:11px;font-weight:600;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:0.3px;border-bottom:1px solid var(--border);user-select:none;white-space:nowrap;}
|
||||
.lib-table th.sortable{cursor:pointer;}
|
||||
.lib-table th.sortable:hover{color:var(--text-primary);}
|
||||
.lib-table td{padding:0;border-bottom:1px solid var(--border);}
|
||||
/* ── Table (div-based for hover effects) ── */
|
||||
.lib-table{width:100%;}
|
||||
.lib-table-header{display:flex;align-items:center;border-bottom:1px solid var(--border);padding:0;}
|
||||
.lib-table-header .th{padding:6px 8px;font-size:11px;font-weight:600;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:0.3px;user-select:none;white-space:nowrap;}
|
||||
.lib-table-header .th.sortable{cursor:pointer;}
|
||||
.lib-table-header .th.sortable:hover{color:var(--text-primary);}
|
||||
.lib-table-body{}
|
||||
|
||||
/* Column widths */
|
||||
.lib-col-select{width:32px;text-align:center;}
|
||||
.lib-col-drag{width:28px;}
|
||||
.lib-col-name{width:auto;}
|
||||
.lib-col-author{width:160px;}
|
||||
.lib-col-source{width:130px;}
|
||||
.lib-col-edited{width:100px;}
|
||||
.lib-col-visited{width:100px;}
|
||||
.lib-col-open{width:80px;text-align:right;}
|
||||
.lib-col-select{width:32px;text-align:center;flex-shrink:0;}
|
||||
.lib-col-drag{width:28px;flex-shrink:0;}
|
||||
.lib-col-name{flex:1;min-width:0;}
|
||||
.lib-col-author{width:160px;flex-shrink:0;}
|
||||
.lib-col-source{width:130px;flex-shrink:0;}
|
||||
.lib-col-edited{width:100px;flex-shrink:0;}
|
||||
.lib-col-visited{width:100px;flex-shrink:0;}
|
||||
.lib-col-open{width:80px;text-align:right;flex-shrink:0;}
|
||||
|
||||
/* ── Row ── */
|
||||
.lib-row{display:flex;align-items:center;height:36px;transition:background 0.1s;position:relative;}
|
||||
@@ -136,7 +138,8 @@
|
||||
/* ── Responsive ── */
|
||||
@media(max-width:768px){
|
||||
.lib-col-author,.lib-col-source,.lib-col-visited{display:none;}
|
||||
.lib-table th:nth-child(4),.lib-table th:nth-child(5),.lib-table th:nth-child(7){display:none;}
|
||||
.lib-table-header .th:nth-child(4),.lib-table-header .th:nth-child(5),.lib-table-header .th:nth-child(7){display:none;}
|
||||
.lib-row .row-cell.lib-col-author,.lib-row .row-cell.lib-col-source,.lib-row .row-cell.lib-col-visited{display:none;}
|
||||
.peek-overlay{width:100vw;max-width:100vw;}
|
||||
}
|
||||
</style>
|
||||
@@ -201,36 +204,34 @@
|
||||
</div>
|
||||
|
||||
<!-- ── Table ── -->
|
||||
<table class="lib-table" x-show="!loading && flatItems.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="lib-col-drag"></th>
|
||||
<th class="lib-col-select">
|
||||
<div class="lib-checkbox" :class="{ checked: allSelected }" @click="toggleSelectAll()"></div>
|
||||
</th>
|
||||
<th class="lib-col-name sortable" @click="sortBy='name';doSort()">Page name</th>
|
||||
<th class="lib-col-author sortable" @click="sortBy='author';doSort()">Created by</th>
|
||||
<th class="lib-col-source">Source</th>
|
||||
<th class="lib-col-edited sortable" @click="sortBy='edited';doSort()">Last edited</th>
|
||||
<th class="lib-col-visited sortable" @click="sortBy='visited';doSort()">Last visited</th>
|
||||
<th class="lib-col-open"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div class="lib-table" x-show="!loading && flatItems.length > 0">
|
||||
<div class="lib-table-header">
|
||||
<div class="th lib-col-drag"></div>
|
||||
<div class="th lib-col-select">
|
||||
<div class="lib-checkbox" :class="{ checked: allSelected }" @click="toggleSelectAll()"></div>
|
||||
</div>
|
||||
<div class="th lib-col-name sortable" @click="sortBy='name';doSort()">Page name</div>
|
||||
<div class="th lib-col-author sortable" @click="sortBy='author';doSort()">Created by</div>
|
||||
<div class="th lib-col-source">Source</div>
|
||||
<div class="th lib-col-edited sortable" @click="sortBy='edited';doSort()">Last edited</div>
|
||||
<div class="th lib-col-visited sortable" @click="sortBy='visited';doSort()">Last visited</div>
|
||||
<div class="th lib-col-open"></div>
|
||||
</div>
|
||||
<div class="lib-table-body">
|
||||
<template x-for="(item, idx) in flatItems" :key="item.id">
|
||||
<tr>
|
||||
<td class="lib-col-drag">
|
||||
<div class="lib-row" :class="{ selected: !!selected[item.id] }">
|
||||
<div class="row-cell lib-col-drag">
|
||||
<div class="drag-handle hover-only" style="margin:auto;" title="Drag to move">
|
||||
<span class="dot"></span><span class="dot"></span>
|
||||
<span class="dot"></span><span class="dot"></span>
|
||||
<span class="dot"></span><span class="dot"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="lib-col-select">
|
||||
</div>
|
||||
<div class="row-cell lib-col-select">
|
||||
<div class="lib-checkbox" :class="{ checked: selected[item.id] }"
|
||||
@click.stop="toggleItem(item)"></div>
|
||||
</td>
|
||||
<td :class="'lib-col-name indent-' + (item.depth || 0)">
|
||||
</div>
|
||||
<div class="row-cell lib-col-name" :class="'indent-' + (item.depth || 0)">
|
||||
<div class="row-name">
|
||||
<!-- Expand chevron -->
|
||||
<button x-show="item.has_children" class="tree-chevron"
|
||||
@@ -241,8 +242,7 @@
|
||||
<span class="row-icon" x-text="item.icon || '📄'"></span>
|
||||
<!-- Title or inline rename -->
|
||||
<span class="row-title" x-show="renamingId !== item.id"
|
||||
@dblclick.stop="startRename(item)"
|
||||
@click.stop="openItem(item)"
|
||||
@click.stop="startRename(item)"
|
||||
x-text="item.title"></span>
|
||||
<input x-show="renamingId === item.id" class="row-title"
|
||||
x-model="renameValue"
|
||||
@@ -251,29 +251,29 @@
|
||||
@blur="commitRename(item)"
|
||||
@click.stop>
|
||||
</div>
|
||||
</td>
|
||||
<td class="lib-col-author">
|
||||
</div>
|
||||
<div class="row-cell lib-col-author">
|
||||
<div class="row-author">
|
||||
<div class="author-avatar" x-text="item.author_initial || '?'"></div>
|
||||
<span x-text="item.author || '—'"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="lib-col-source">
|
||||
</div>
|
||||
<div class="row-cell lib-col-source">
|
||||
<span class="row-source" x-text="item.source_label || 'Private'"></span>
|
||||
</td>
|
||||
<td class="lib-col-edited">
|
||||
</div>
|
||||
<div class="row-cell lib-col-edited">
|
||||
<span class="row-time" x-text="formatTime(item.updated_at)"></span>
|
||||
</td>
|
||||
<td class="lib-col-visited">
|
||||
</div>
|
||||
<div class="row-cell lib-col-visited">
|
||||
<span class="row-time" x-text="formatTime(item.visited_at)"></span>
|
||||
</td>
|
||||
<td class="lib-col-open" style="text-align:right;">
|
||||
</div>
|
||||
<div class="row-cell lib-col-open" style="text-align:right;">
|
||||
<button class="btn-open" @click.stop="openPeek(item)">⤢ Open</button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- "+ Add new" -->
|
||||
<div class="add-new-row" x-show="!loading && tab !== 'favorites' && tab !== 'shared' && tab !== 'published'"
|
||||
@@ -332,6 +332,7 @@
|
||||
function libraryPage() {
|
||||
return {
|
||||
tab: 'recents',
|
||||
workspaceId: 0,
|
||||
items: [],
|
||||
loading: false,
|
||||
searchOpen: false,
|
||||
@@ -410,6 +411,8 @@ function libraryPage() {
|
||||
|
||||
// ── Init ──
|
||||
init() {
|
||||
var wsEl = document.getElementById('lib-workspace-id');
|
||||
this.workspaceId = parseInt(wsEl ? wsEl.textContent : '0') || 0;
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var t = params.get('tab');
|
||||
if (t && ['recents','favorites','shared','published','private','workspace'].indexOf(t) !== -1) {
|
||||
@@ -441,7 +444,10 @@ function libraryPage() {
|
||||
async loadTab() {
|
||||
this.loading = true;
|
||||
try {
|
||||
var r = await fetch('/api/library/' + this.tab + '?tree=1');
|
||||
var url = '/api/library/' + this.tab;
|
||||
var sep = '?';
|
||||
if (this.workspaceId) { url += '?workspace_id=' + this.workspaceId; sep = '&'; }
|
||||
var r = await fetch(url);
|
||||
var d = await r.json();
|
||||
this.items = d.items || [];
|
||||
this._markTree(this.items, 0);
|
||||
|
||||
Reference in New Issue
Block a user