feat(library): tags + tag filtering, monochrome icons; fix live tag updates
- library: expose page tags (batch), render tag chips per row and add a tag filter bar; register tag create/associate/remove in the shared context menu exactly like local-workspace - library: restore monochrome SVG icons (folder/edit/image/file) matching local-workspace, with optional custom page_icon - workspace: refresh tag chips/filter live on tag add/remove (reassign arrays + tagsVersion) instead of requiring a page reload - ctxmenu: measure the rendered menu and clamp it to the viewport on open
This commit is contained in:
+31
-2
@@ -69,6 +69,7 @@ def _build_item(db_row: dict, uid: int = 1) -> dict:
|
|||||||
"id": page_id,
|
"id": page_id,
|
||||||
"title": title,
|
"title": title,
|
||||||
"icon": icon,
|
"icon": icon,
|
||||||
|
"page_icon": db_row.get("page_icon") or "",
|
||||||
"is_folder": bool(db_row.get("is_folder", 0)),
|
"is_folder": bool(db_row.get("is_folder", 0)),
|
||||||
"source_type": source_type,
|
"source_type": source_type,
|
||||||
"source_label": _source_label(source_type, workspace),
|
"source_label": _source_label(source_type, workspace),
|
||||||
@@ -84,6 +85,7 @@ def _build_item(db_row: dict, uid: int = 1) -> dict:
|
|||||||
"content_format": content_format,
|
"content_format": content_format,
|
||||||
"favorited": bool(db_row.get("favorited", 0)),
|
"favorited": bool(db_row.get("favorited", 0)),
|
||||||
"share_mode": db_row.get("share_mode", "private"),
|
"share_mode": db_row.get("share_mode", "private"),
|
||||||
|
"tags": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +100,30 @@ def _apply_source_filter(query: str, params: list, source_type: str) -> tuple[st
|
|||||||
|
|
||||||
|
|
||||||
def _rows_to_items(rows, uid: int = 1) -> list:
|
def _rows_to_items(rows, uid: int = 1) -> list:
|
||||||
return [_build_item(dict(r), uid) for r in rows]
|
items = [_build_item(dict(r), uid) for r in rows]
|
||||||
|
return _attach_tags(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_tags(items: list) -> list:
|
||||||
|
"""Batch-load page tags for library items."""
|
||||||
|
if not items:
|
||||||
|
return items
|
||||||
|
ids = [it["id"] for it in items]
|
||||||
|
placeholders = ",".join("?" for _ in ids)
|
||||||
|
with get_conn() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
f"SELECT pt.page_id, t.id, t.name, t.color FROM page_tags pt "
|
||||||
|
f"JOIN tags t ON t.id = pt.tag_id WHERE pt.page_id IN ({placeholders})",
|
||||||
|
ids,
|
||||||
|
).fetchall()
|
||||||
|
tag_map: dict = {}
|
||||||
|
for r in rows:
|
||||||
|
tag_map.setdefault(r["page_id"], []).append({
|
||||||
|
"id": r["id"], "name": r["name"], "color": r["color"],
|
||||||
|
})
|
||||||
|
for it in items:
|
||||||
|
it["tags"] = tag_map.get(it["id"], [])
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
def _enrich_children(items: list) -> list:
|
def _enrich_children(items: list) -> list:
|
||||||
@@ -124,7 +149,7 @@ def _enrich_children(items: list) -> list:
|
|||||||
|
|
||||||
BASE_SELECT = (
|
BASE_SELECT = (
|
||||||
"SELECT p.id, p.title, p.workspace, p.updated_at, p.content_format, "
|
"SELECT p.id, p.title, p.workspace, p.updated_at, p.content_format, "
|
||||||
"p.parent_id, p.share_mode, p.parent_section"
|
"p.parent_id, p.share_mode, p.parent_section, p.page_icon"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -361,6 +386,8 @@ async def library_local_workspace_children(item_id: int, request: Request):
|
|||||||
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
||||||
"content_format": r["content_format"] or "file",
|
"content_format": r["content_format"] or "file",
|
||||||
"favorited": False,
|
"favorited": False,
|
||||||
|
"page_icon": "",
|
||||||
|
"tags": [],
|
||||||
"size_display": _format_size(r["size"]) if r["size"] else "",
|
"size_display": _format_size(r["size"]) if r["size"] else "",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -469,6 +496,8 @@ async def library_local_workspace(
|
|||||||
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
||||||
"content_format": r["content_format"] or "file",
|
"content_format": r["content_format"] or "file",
|
||||||
"favorited": False,
|
"favorited": False,
|
||||||
|
"page_icon": "",
|
||||||
|
"tags": [],
|
||||||
"size_display": _format_size(r["size"]) if r["size"] else "",
|
"size_display": _format_size(r["size"]) if r["size"] else "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,27 @@
|
|||||||
this.tagExistingOpen = false;
|
this.tagExistingOpen = false;
|
||||||
this.iconOpen = false;
|
this.iconOpen = false;
|
||||||
this.newTagColor = (node && node.tags && node.tags[0] && node.tags[0].color) || '#787774';
|
this.newTagColor = (node && node.tags && node.tags[0] && node.tags[0].color) || '#787774';
|
||||||
this.x = Math.max(4, Math.min((ev && ev.clientX) || 0, window.innerWidth - 240));
|
var cx = (ev && ev.clientX) || 0;
|
||||||
this.y = Math.max(4, Math.min((ev && ev.clientY) || 0, window.innerHeight - 48));
|
var cy = (ev && ev.clientY) || 0;
|
||||||
|
this.x = Math.max(8, Math.min(cx, window.innerWidth - 240));
|
||||||
|
this.y = Math.max(8, Math.min(cy, window.innerHeight - 48));
|
||||||
this.open = true;
|
this.open = true;
|
||||||
|
var self = this;
|
||||||
|
/* Mesure la taille réelle du menu (une fois rendu) puis le replace
|
||||||
|
pour qu'il reste TOUJOURS entièrement visible dans le viewport. */
|
||||||
|
var place = function () {
|
||||||
|
var el = document.querySelector('.fd-ctx-menu');
|
||||||
|
var w = el ? el.offsetWidth : 240;
|
||||||
|
var h = el ? el.offsetHeight : 320;
|
||||||
|
var vw = window.innerWidth, vh = window.innerHeight;
|
||||||
|
var nx = cx, ny = cy;
|
||||||
|
if (nx + w > vw - 8) nx = vw - w - 8;
|
||||||
|
if (ny + h > vh - 8) ny = vh - h - 8;
|
||||||
|
self.x = Math.max(8, nx);
|
||||||
|
self.y = Math.max(8, ny);
|
||||||
|
};
|
||||||
|
if (window.Alpine && window.Alpine.nextTick) window.Alpine.nextTick(place);
|
||||||
|
else setTimeout(place, 0);
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
|
|||||||
+130
-18
@@ -88,6 +88,13 @@
|
|||||||
.row-title:hover{text-decoration:underline;}
|
.row-title:hover{text-decoration:underline;}
|
||||||
.row-title input{width:100%;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;}
|
.row-title input{width:100%;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;}
|
||||||
|
|
||||||
|
/* ── Tag chips (représentation + filtre) ── */
|
||||||
|
.row-tags{display:flex;align-items:center;gap:3px;flex-shrink:0;overflow:hidden;max-width:45%;}
|
||||||
|
.tag-chip{display:inline-flex;align-items:center;gap:3px;font-size:10px;line-height:1;padding:2px 6px;border-radius:999px;color:#fff;white-space:nowrap;border:none;cursor:pointer;font-family:inherit;}
|
||||||
|
.tag-chip.active{outline:2px solid var(--text-primary);outline-offset:1px;}
|
||||||
|
.lib-tag-filter{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:4px 16px 2px;}
|
||||||
|
.lib-tag-filter .tf-label{font-size:11px;color:var(--text-dim);margin-right:2px;}
|
||||||
|
|
||||||
/* ── Shared direction sub-filter pils ── */
|
/* ── Shared direction sub-filter pils ── */
|
||||||
.lib-share-directory{display:flex;align-items:center;gap:6px;padding:4px 16px;}
|
.lib-share-directory{display:flex;align-items:center;gap:6px;padding:4px 16px;}
|
||||||
.lib-dir-pill{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;transition:background 0.15s,color 0.15s,border-color 0.15s;}
|
.lib-dir-pill{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;transition:background 0.15s,color 0.15s,border-color 0.15s;}
|
||||||
@@ -271,6 +278,20 @@
|
|||||||
<button class="lib-dir-pill" :class="{ active: shareDir === 'received' }" @click="setShareDir('received')">Avec moi</button>
|
<button class="lib-dir-pill" :class="{ active: shareDir === 'received' }" @click="setShareDir('received')">Avec moi</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tag filter bar -->
|
||||||
|
<div class="lib-tag-filter" x-show="workspaceTags && workspaceTags.length > 0" x-cloak>
|
||||||
|
<span class="tf-label">Tags:</span>
|
||||||
|
<template x-for="t in workspaceTags" :key="'ltf'+t.id">
|
||||||
|
<button class="tag-chip" :class="{ active: tagFilter === t.name }"
|
||||||
|
:style="{ background: t.color }"
|
||||||
|
@click="toggleTagFilter(t.name)">
|
||||||
|
<span x-text="t.name"></span>
|
||||||
|
<span style="font-size:10px;opacity:.7" x-text="'('+(t.count||0)+')'"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button x-show="tagFilter" class="tag-chip" style="background:var(--bg-tertiary);color:var(--text-secondary);" @click="tagFilter='', _recomputeFlatItems()">clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Search bar (toggleable) -->
|
<!-- Search bar (toggleable) -->
|
||||||
<div class="lib-search-bar" x-show="searchOpen" x-transition>
|
<div class="lib-search-bar" x-show="searchOpen" x-transition>
|
||||||
<input id="lib-search-input" type="text" placeholder="Search in this list..." x-model="searchQuery" @input="doSearch()">
|
<input id="lib-search-input" type="text" placeholder="Search in this list..." x-model="searchQuery" @input="doSearch()">
|
||||||
@@ -376,7 +397,7 @@
|
|||||||
<div class="move-modal-item" @click="confirmMove(0)">{{ fd_icon("file",14) }} <span>Root (no parent)</span></div>
|
<div class="move-modal-item" @click="confirmMove(0)">{{ fd_icon("file",14) }} <span>Root (no parent)</span></div>
|
||||||
<template x-for="it in moveCandidates" :key="it.id">
|
<template x-for="it in moveCandidates" :key="it.id">
|
||||||
<div class="move-modal-item" @click="confirmMove(it.id)">
|
<div class="move-modal-item" @click="confirmMove(it.id)">
|
||||||
<span x-html="it.icon ? getSvgIcon(it.icon,14) : getSvgIcon('file',14)"></span> <span x-text="it.title"></span>
|
<span x-html="_renderIcon(it)"></span> <span x-text="it.title"></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -393,7 +414,7 @@
|
|||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="peek-title">
|
<div class="peek-title">
|
||||||
<span x-html="peekItem.icon ? getSvgIcon(peekItem.icon,14) : getSvgIcon('file',14)"></span>
|
<span x-html="_renderIcon(peekItem)"></span>
|
||||||
<span x-text="peekItem.title"></span>
|
<span x-text="peekItem.title"></span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="openItem(peekItem)" title="Open full page">
|
<button @click="openItem(peekItem)" title="Open full page">
|
||||||
@@ -467,6 +488,10 @@ function libraryPage() {
|
|||||||
showAuthor: false,
|
showAuthor: false,
|
||||||
showSource: false,
|
showSource: false,
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
workspaceTags: [],
|
||||||
|
tagFilter: '',
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
sortBy: '',
|
sortBy: '',
|
||||||
|
|
||||||
@@ -553,19 +578,24 @@ function libraryPage() {
|
|||||||
} else {
|
} else {
|
||||||
html += '<span style="width:18px;flex-shrink:0;"></span>';
|
html += '<span style="width:18px;flex-shrink:0;"></span>';
|
||||||
}
|
}
|
||||||
// Icon — folders get a folder icon
|
// Icon — monochrome, comme local-workspace (emoji custom si défini)
|
||||||
var isFolder = item.is_folder || item.has_children;
|
var isFolder = item.is_folder || item.has_children;
|
||||||
if (isFolder) {
|
html += '<span class="row-icon">' + self._renderIcon(item) + '</span>';
|
||||||
html += '<span class="row-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg></span>';
|
|
||||||
} else {
|
|
||||||
html += '<span class="row-icon">' + self._renderIcon(item.icon, 'file') + '</span>';
|
|
||||||
}
|
|
||||||
// Title or rename input
|
// Title or rename input
|
||||||
if (isRenaming) {
|
if (isRenaming) {
|
||||||
html += '<input id="rename-inp-' + item.id + '" value="' + self._escAttr(self.renameValue) + '" onkeydown="if(event.key===\'Enter\')window._libData.commitRenameById(' + item.id + ');if(event.key===\'Escape\')window._libData.cancelRename();" onblur="window._libData.commitRenameById(' + item.id + ')" style="flex:1;min-width:80px;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;" onclick="event.stopPropagation()">';
|
html += '<input id="rename-inp-' + item.id + '" value="' + self._escAttr(self.renameValue) + '" onkeydown="if(event.key===\'Enter\')window._libData.commitRenameById(' + item.id + ');if(event.key===\'Escape\')window._libData.cancelRename();" onblur="window._libData.commitRenameById(' + item.id + ')" style="flex:1;min-width:80px;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;" onclick="event.stopPropagation()">';
|
||||||
} else {
|
} else {
|
||||||
html += '<span class="row-title" title="' + self._escAttr(item.title) + '">' + self._escHtml(item.title) + '</span>';
|
html += '<span class="row-title" title="' + self._escAttr(item.title) + '">' + self._escHtml(item.title) + '</span>';
|
||||||
}
|
}
|
||||||
|
// Tags
|
||||||
|
if (item.tags && item.tags.length) {
|
||||||
|
html += '<span class="row-tags">';
|
||||||
|
for (var ti = 0; ti < item.tags.length; ti++) {
|
||||||
|
var tg = item.tags[ti];
|
||||||
|
html += '<span class="tag-chip" style="background:' + self._escAttr(tg.color || '#787774') + '" title="' + self._escAttr(tg.name) + '">' + self._escHtml(tg.name) + '</span>';
|
||||||
|
}
|
||||||
|
html += '</span>';
|
||||||
|
}
|
||||||
// OPEN button (side peek) — hidden for folders
|
// OPEN button (side peek) — hidden for folders
|
||||||
if (!isFolder) {
|
if (!isFolder) {
|
||||||
html += '<button class="btn-open hover-only" onclick="event.stopPropagation();window._libData.openPeekById(' + item.id + ')" title="Open in side peek">';
|
html += '<button class="btn-open hover-only" onclick="event.stopPropagation();window._libData.openPeekById(' + item.id + ')" title="Open in side peek">';
|
||||||
@@ -587,14 +617,15 @@ function libraryPage() {
|
|||||||
|
|
||||||
_escHtml(s) { var d=document.createElement('div'); d.textContent = s||''; return d.innerHTML; },
|
_escHtml(s) { var d=document.createElement('div'); d.textContent = s||''; return d.innerHTML; },
|
||||||
_escAttr(s) { return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); },
|
_escAttr(s) { return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); },
|
||||||
_renderIcon(icon, fallback) {
|
_renderIcon(item) {
|
||||||
var name = icon || fallback || 'file';
|
if (!item) return getSvgIcon('file', 14);
|
||||||
// Only allow known icon names, otherwise use fallback
|
|
||||||
var valid = ['folder','file','star','link','trash','book','home','settings','lock','globe','image','edit','calendar','users','user','search','tag','bar-chart','grid','list','align-left','zap','paperclip','key','refresh','upload'];
|
|
||||||
if (valid.indexOf(name) !== -1) return getSvgIcon(name, 14);
|
|
||||||
// Custom emoji / non-ASCII icon (e.g. 📝) — render as-is
|
// Custom emoji / non-ASCII icon (e.g. 📝) — render as-is
|
||||||
if (/[^\x00-\x7F]/.test(name)) return '<span style="font-size:14px;line-height:1;">' + this._escHtml(name) + '</span>';
|
if (item.page_icon) return '<span style="font-size:14px;line-height:1;">' + this._escHtml(item.page_icon) + '</span>';
|
||||||
return getSvgIcon(fallback || 'file', 14);
|
if (item.is_folder || item.has_children) return getSvgIcon('folder', 14);
|
||||||
|
if (item.content_format && item.content_format !== 'file') return getSvgIcon('edit', 14);
|
||||||
|
var n = (item.title || '').toLowerCase();
|
||||||
|
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return getSvgIcon('image', 14);
|
||||||
|
return getSvgIcon('file', 14);
|
||||||
},
|
},
|
||||||
|
|
||||||
_recomputeFlatItems() {
|
_recomputeFlatItems() {
|
||||||
@@ -606,6 +637,12 @@ function libraryPage() {
|
|||||||
return (r.title || '').toLowerCase().indexOf(q) !== -1;
|
return (r.title || '').toLowerCase().indexOf(q) !== -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (this.tagFilter) {
|
||||||
|
var tf = this.tagFilter;
|
||||||
|
result = result.filter(function(r) {
|
||||||
|
return (r.tags || []).some(function(t) { return t.name === tf; });
|
||||||
|
});
|
||||||
|
}
|
||||||
// Use splice for Alpine 3.14.9 reactivity (array reference reassign fails)
|
// Use splice for Alpine 3.14.9 reactivity (array reference reassign fails)
|
||||||
this.flatItems.splice(0, this.flatItems.length, ...result);
|
this.flatItems.splice(0, this.flatItems.length, ...result);
|
||||||
this.selectedCount = Object.keys(this.selected).length;
|
this.selectedCount = Object.keys(this.selected).length;
|
||||||
@@ -683,10 +720,15 @@ function libraryPage() {
|
|||||||
link: function (n) { self.copyLink(n); },
|
link: function (n) { self.copyLink(n); },
|
||||||
move: function (n) { self.openMovePicker([n.id]); },
|
move: function (n) { self.openMovePicker([n.id]); },
|
||||||
fav: function (n) { self.toggleFavoriteItem(n); },
|
fav: function (n) { self.toggleFavoriteItem(n); },
|
||||||
delete: function (n) { self.deleteItem(n); }
|
delete: function (n) { self.deleteItem(n); },
|
||||||
|
tagExisting: function (t) { self.ctxAddExistingTag(t); },
|
||||||
|
tagAdd: function (name, color) { self.ctxAddNewTag(name, color); },
|
||||||
|
tagRemove: function (tagId) { self.ctxRemoveTag(tagId); }
|
||||||
};
|
};
|
||||||
if (this.tab === 'recents') handlers.recent = function (n) { self.removeItemFromRecents(n); };
|
if (this.tab === 'recents') handlers.recent = function (n) { self.removeItemFromRecents(n); };
|
||||||
Alpine.store('fdCtx').openMenu(evt, item, 'library', handlers);
|
var store = Alpine.store('fdCtx');
|
||||||
|
store.setAvail(self._ctxAvailTags(item));
|
||||||
|
store.openMenu(evt, item, 'library', handlers);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Drag & drop (move / reparent) ──
|
// ── Drag & drop (move / reparent) ──
|
||||||
@@ -821,6 +863,7 @@ function libraryPage() {
|
|||||||
this.tab = t;
|
this.tab = t;
|
||||||
}
|
}
|
||||||
this._recomputeEmpty();
|
this._recomputeEmpty();
|
||||||
|
this.loadWorkspaceTags();
|
||||||
console.log('[Library] loading tab: ' + this.tab);
|
console.log('[Library] loading tab: ' + this.tab);
|
||||||
this.loadTab();
|
this.loadTab();
|
||||||
},
|
},
|
||||||
@@ -830,6 +873,7 @@ function libraryPage() {
|
|||||||
if (t === 'repository' && !this.isGiteaActive) return;
|
if (t === 'repository' && !this.isGiteaActive) return;
|
||||||
this.tab = t;
|
this.tab = t;
|
||||||
this.shareDir = 'all';
|
this.shareDir = 'all';
|
||||||
|
this.tagFilter = '';
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.selected = {};
|
this.selected = {};
|
||||||
this.selectedCount = 0;
|
this.selectedCount = 0;
|
||||||
@@ -1075,7 +1119,7 @@ function libraryPage() {
|
|||||||
body: JSON.stringify({icon: icon})
|
body: JSON.stringify({icon: icon})
|
||||||
});
|
});
|
||||||
if (!r.ok) throw new Error('icon update failed');
|
if (!r.ok) throw new Error('icon update failed');
|
||||||
item.icon = icon;
|
item.page_icon = icon;
|
||||||
this._renderTable();
|
this._renderTable();
|
||||||
if (window.showToast) window.showToast('Icon updated', 'success');
|
if (window.showToast) window.showToast('Icon updated', 'success');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -1083,6 +1127,74 @@ function libraryPage() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Tags ──
|
||||||
|
async loadWorkspaceTags() {
|
||||||
|
try {
|
||||||
|
var r = await fetch('/api/settings/tags/all');
|
||||||
|
var d = await r.json();
|
||||||
|
this.workspaceTags = d.tags || [];
|
||||||
|
} catch(e) { this.workspaceTags = []; }
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTagFilter(tagName) {
|
||||||
|
this.tagFilter = (this.tagFilter === tagName) ? '' : tagName;
|
||||||
|
this._recomputeFlatItems();
|
||||||
|
},
|
||||||
|
|
||||||
|
_ctxAvailTags(item) {
|
||||||
|
return (this.workspaceTags || []).filter(function(t) {
|
||||||
|
return !((item && item.tags) || []).some(function(nt) { return nt.id === t.id; });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_ctxRefreshAvail() {
|
||||||
|
var store = window.Alpine && Alpine.store('fdCtx');
|
||||||
|
if (store && store.node) store.setAvail(this._ctxAvailTags(store.node));
|
||||||
|
},
|
||||||
|
|
||||||
|
ctxAddExistingTag(tag) {
|
||||||
|
this.ctxAddNewTag(tag.name, tag.color);
|
||||||
|
},
|
||||||
|
|
||||||
|
ctxRemoveTag(tagId) {
|
||||||
|
var store = window.Alpine && Alpine.store('fdCtx');
|
||||||
|
var item = store && store.node;
|
||||||
|
if (!item) return;
|
||||||
|
var self = this;
|
||||||
|
fetch('/api/local-workspace/items/' + item.id + '/tags/' + tagId, { method: 'DELETE' })
|
||||||
|
.then(function(r) {
|
||||||
|
if (!r.ok) return;
|
||||||
|
item.tags = (item.tags || []).filter(function(t) { return t.id !== tagId; });
|
||||||
|
self.loadWorkspaceTags();
|
||||||
|
self._ctxRefreshAvail();
|
||||||
|
self._recomputeFlatItems();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async ctxAddNewTag(tagName, color) {
|
||||||
|
tagName = (tagName || '').trim();
|
||||||
|
if (!tagName) return;
|
||||||
|
var store = window.Alpine && Alpine.store('fdCtx');
|
||||||
|
var item = store && store.node;
|
||||||
|
if (!item) return;
|
||||||
|
color = color || (store && store.newTagColor) || '#787774';
|
||||||
|
try {
|
||||||
|
var r = await fetch('/api/local-workspace/items/' + item.id + '/tags', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({name: tagName, color: color})
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
var d = await r.json();
|
||||||
|
var cur = item.tags || [];
|
||||||
|
if (!cur.some(function(t) { return t.id === d.tag.id; })) item.tags = cur.concat([d.tag]);
|
||||||
|
await this.loadWorkspaceTags();
|
||||||
|
this._ctxRefreshAvail();
|
||||||
|
this._recomputeFlatItems();
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
},
|
||||||
|
|
||||||
async toggleFavoriteItem(item) {
|
async toggleFavoriteItem(item) {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
var csrf = this._getCsrf();
|
var csrf = this._getCsrf();
|
||||||
|
|||||||
@@ -532,6 +532,7 @@ window._wsInitData = (function() {
|
|||||||
workspaceTags: [],
|
workspaceTags: [],
|
||||||
tagFilter: '',
|
tagFilter: '',
|
||||||
tagInputId: null,
|
tagInputId: null,
|
||||||
|
tagsVersion: 0,
|
||||||
|
|
||||||
// Preview panel
|
// Preview panel
|
||||||
previewItem: null,
|
previewItem: null,
|
||||||
@@ -1158,11 +1159,13 @@ window._wsInitData = (function() {
|
|||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
if (!node.tags) node.tags = [];
|
var cur = node.tags || [];
|
||||||
if (!node.tags.some(function(t) { return t.id === d.tag.id; })) node.tags.push(d.tag);
|
if (!cur.some(function(t) { return t.id === d.tag.id; })) node.tags = cur.concat([d.tag]);
|
||||||
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
|
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
|
||||||
this.displayTree = [...this.tree];
|
this.displayTree = [...this.tree];
|
||||||
this.loadWorkspaceTags();
|
this.tagsVersion++;
|
||||||
|
await this.loadWorkspaceTags();
|
||||||
|
this._computeFilteredTableItems();
|
||||||
this._ctxRefreshAvail();
|
this._ctxRefreshAvail();
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
@@ -2241,7 +2244,9 @@ window._wsInitData = (function() {
|
|||||||
// Update node tags in tree
|
// Update node tags in tree
|
||||||
this._updateNodeTags(this.tree, itemId, d.tag, 'add');
|
this._updateNodeTags(this.tree, itemId, d.tag, 'add');
|
||||||
this.displayTree = [...this.tree];
|
this.displayTree = [...this.tree];
|
||||||
this.loadWorkspaceTags();
|
this.tagsVersion++;
|
||||||
|
await this.loadWorkspaceTags();
|
||||||
|
this._computeFilteredTableItems();
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
this.tagInputId = null;
|
this.tagInputId = null;
|
||||||
@@ -2255,7 +2260,9 @@ window._wsInitData = (function() {
|
|||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
this._updateNodeTags(this.tree, itemId, null, 'remove', tagId);
|
this._updateNodeTags(this.tree, itemId, null, 'remove', tagId);
|
||||||
this.displayTree = [...this.tree];
|
this.displayTree = [...this.tree];
|
||||||
this.loadWorkspaceTags();
|
this.tagsVersion++;
|
||||||
|
await this.loadWorkspaceTags();
|
||||||
|
this._computeFilteredTableItems();
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
},
|
},
|
||||||
@@ -2264,10 +2271,10 @@ window._wsInitData = (function() {
|
|||||||
for (var i=0; i<nodes.length; i++) {
|
for (var i=0; i<nodes.length; i++) {
|
||||||
if (nodes[i].id === itemId) {
|
if (nodes[i].id === itemId) {
|
||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
if (!nodes[i].tags) nodes[i].tags = [];
|
// Réassigne un nouveau tableau → déclenche la réactivité Alpine
|
||||||
// Avoid duplicates
|
var cur = nodes[i].tags || [];
|
||||||
if (!nodes[i].tags.some(function(t) { return t.id === tag.id; })) {
|
if (!cur.some(function(t) { return t.id === tag.id; })) {
|
||||||
nodes[i].tags.push(tag);
|
nodes[i].tags = cur.concat([tag]);
|
||||||
}
|
}
|
||||||
} else if (action === 'remove') {
|
} else if (action === 'remove') {
|
||||||
nodes[i].tags = (nodes[i].tags || []).filter(function(t) { return t.id !== removeTagId; });
|
nodes[i].tags = (nodes[i].tags || []).filter(function(t) { return t.id !== removeTagId; });
|
||||||
@@ -2682,7 +2689,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
|||||||
</div>
|
</div>
|
||||||
<!-- Children -->
|
<!-- Children -->
|
||||||
<ul class="ws-tree" x-show="expanded[node.id]" x-transition
|
<ul class="ws-tree" x-show="expanded[node.id]" x-transition
|
||||||
x-html="renderChildren(node.children, (node.depth||0)+1)">
|
x-html="renderChildren(node.children, (node.depth||0)+1, tagsVersion)">
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user