fix(ctxmenu): repair library rendering and complete unified row menu
- library.html: remove leftover syntax error + orphan </template></div> that broke libraryPage() (page showed 'undefined' and no files), call the correct fdCtx.openMenu and use self instead of a throwaway libraryPage() instance - _ctx_menu.html: full conventional menu with shortcuts, tags + colour picker, edit-icon picker; closes on outside click and Escape - local_workspace.html: migrate onContextMenu to the shared fdCtx store with complete handlers; drop legacy window._ctxMenuData DOM hacks - dashboard.py: expose page_icon + favorited in the workspace tree
This commit is contained in:
@@ -992,16 +992,17 @@ async def local_workspace_tree(request: Request, folder: int = None):
|
||||
ws_id = ws["id"] if ws else None
|
||||
if not ws_id:
|
||||
return {"tree": [], "breadcrumb": []}
|
||||
uid = _get_user_id(request)
|
||||
|
||||
with get_conn() as conn:
|
||||
if folder:
|
||||
# Show only this folder's children + build breadcrumb
|
||||
children = _build_tree_children(conn, folder, ws_id)
|
||||
children = _build_tree_children(conn, folder, ws_id, uid)
|
||||
breadcrumb = _build_breadcrumb(conn, folder)
|
||||
return {"tree": children, "breadcrumb": breadcrumb, "current_folder": folder}
|
||||
else:
|
||||
# Full tree from root
|
||||
roots = _build_tree_children(conn, None, ws_id)
|
||||
roots = _build_tree_children(conn, None, ws_id, uid)
|
||||
return {"tree": roots, "breadcrumb": [], "current_folder": None}
|
||||
|
||||
|
||||
@@ -1028,11 +1029,11 @@ async def local_workspace_breadcrumb(request: Request, folder: int):
|
||||
return {"breadcrumb": breadcrumb}
|
||||
|
||||
|
||||
def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
||||
def _build_tree_children(conn, parent_id: int | None, ws_id: int, uid: int | None = None) -> list:
|
||||
"""Recursively build the tree of children for a node."""
|
||||
if parent_id is None:
|
||||
rows = conn.execute(
|
||||
"SELECT id, title, parent_section, content_format, content, "
|
||||
"SELECT id, title, parent_section, content_format, content, page_icon, "
|
||||
"(is_shared OR share_mode != 'private' OR COALESCE(published,0)) as is_shared, "
|
||||
"created_at, updated_at FROM pages "
|
||||
"WHERE parent_id IS NULL AND workspace_id=? AND deleted_at IS NULL "
|
||||
@@ -1041,7 +1042,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT id, title, parent_section, content_format, content, "
|
||||
"SELECT id, title, parent_section, content_format, content, page_icon, "
|
||||
"(is_shared OR share_mode != 'private' OR COALESCE(published,0)) as is_shared, "
|
||||
"created_at, updated_at FROM pages "
|
||||
"WHERE parent_id=? AND workspace_id=? AND deleted_at IS NULL "
|
||||
@@ -1059,6 +1060,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
||||
# Collect all page IDs to fetch tags in one query
|
||||
all_ids = [r["id"] for r in rows]
|
||||
tags_map = {}
|
||||
favorited_ids = set()
|
||||
if all_ids:
|
||||
placeholders = ",".join("?" for _ in all_ids)
|
||||
tag_rows = conn.execute(
|
||||
@@ -1070,11 +1072,17 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
||||
tags_map.setdefault(tr["page_id"], []).append({
|
||||
"id": tr["id"], "name": tr["name"], "color": tr["color"],
|
||||
})
|
||||
if uid is not None:
|
||||
fav_rows = conn.execute(
|
||||
f"SELECT page_id FROM favorites WHERE user_id=? AND page_id IN ({placeholders})",
|
||||
[uid, *all_ids],
|
||||
).fetchall()
|
||||
favorited_ids = {fr["page_id"] for fr in fav_rows}
|
||||
|
||||
tree = []
|
||||
for r in rows:
|
||||
is_folder = r["parent_section"] == "Workspace"
|
||||
children = _build_tree_children(conn, r["id"], ws_id)
|
||||
children = _build_tree_children(conn, r["id"], ws_id, uid)
|
||||
|
||||
# Compute size
|
||||
size = 0
|
||||
@@ -1094,6 +1102,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
||||
"type": "folder" if is_folder else "page",
|
||||
"is_folder": is_folder,
|
||||
"content_format": r["content_format"] if not is_folder else None,
|
||||
"page_icon": r["page_icon"] or "",
|
||||
"children": children,
|
||||
"has_children": len(children) > 0,
|
||||
"child_count": len(children),
|
||||
@@ -1104,6 +1113,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
||||
"author": author,
|
||||
"tags": tags_map.get(r["id"], []),
|
||||
"is_shared": bool(r["is_shared"]),
|
||||
"favorited": r["id"] in favorited_ids,
|
||||
})
|
||||
return tree
|
||||
|
||||
|
||||
+176
-67
@@ -1,5 +1,5 @@
|
||||
{###############################################################################
|
||||
_ctx_menu.html — LE menu contextuel UNIQUE de FlowDeck (v5.4.x)
|
||||
_ctx_menu.html — LE menu contextuel UNIQUE de FlowDeck
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
Inclus par library.html ET local_workspace.html (les 2 seules pages du
|
||||
contexte fichier). Ce partial est LE SEUL menu : son markup décrit l'UNION
|
||||
@@ -7,21 +7,13 @@
|
||||
chaque page au travers d'un store Alpine enregistré PLUS BAS (IIFE + défense
|
||||
d'exécution unique → include 2 fois = store 1 fois).
|
||||
|
||||
Union des items (identiques sur les 2 pages) :
|
||||
1. Open .................... library: openItem / ws: ctxMenuOpenPage
|
||||
2. Open in side peek ....... library: openPeek (ws: item caché)
|
||||
3. Open folder ............. ws: ctxMenuOpenFolder (dossier uniquement)
|
||||
4. Rename .................. library: startRename / ws: ctxMenuRename
|
||||
5. Add/Remove Favorites .... library: toggleFavoriteItem (ws: caché)
|
||||
6. Duplicate ............... library: duplicateItem / ws: ctxMenuDuplicate
|
||||
7. Copy link ............... library: copyLink (ws: caché)
|
||||
8. Move to ................. library: openMovePicker (ws: caché)
|
||||
9. Tags (section dynamique) ws: ctxTagExisting/… (s'affiche si node.tags)
|
||||
10. Delete .................. library: deleteItem / ws: ctxMenuDelete (danger)
|
||||
Chaque item se masque quand le handler correspondant n'existe pas sur la
|
||||
page (x-show="$store.fdCtx.has('key')") → menu structurellement identique,
|
||||
avec la capacité respective de la page au clic.
|
||||
|
||||
Chaque item masque ses entrées quand `handlers` de l'autre page est vide
|
||||
(x-show="fdCtx.has('key')") → le menu est STRUCTURELLEMENT identique des
|
||||
deux côtés, avec la capacité respective au clic.
|
||||
Handlers attendus (tous optionnels) :
|
||||
open, openTab, peek, folder, rename, setIcon, duplicate, link, move,
|
||||
fav, recent, delete, tagExisting, tagAdd, tagRemove
|
||||
############################################################################}
|
||||
<script data-cfasync="false">
|
||||
/* ═════════════════════════════════════════════════════════════════════
|
||||
@@ -29,9 +21,7 @@
|
||||
Enregistré dans 'alpine:init' avec garde d'exécution : que la page
|
||||
inclue ce partial dans base.html ou directement, le js ne s'exécute
|
||||
qu'une seule fois. Chaque page, à l'ouverture du menu, positionne :
|
||||
fdCtx.open(evtX, evtY, node, pageHash, handlers)
|
||||
où handlers = { open, peek, folder, rename, fav, duplicate, link,
|
||||
move, del, tagAddExisting, tagAddNew, tagRemove, avail }
|
||||
fdCtx.openMenu(evt, node, pageHash, handlers)
|
||||
─────────────────────────────────────────────────────────────────── */
|
||||
(function () {
|
||||
if (window.__fdCtxMenuRegistered) return;
|
||||
@@ -43,21 +33,39 @@
|
||||
|
||||
Alpine.store('fdCtx', {
|
||||
open: false, x: 0, y: 0, node: null, page: null,
|
||||
handlers: {}, _cache: {},
|
||||
handlers: {},
|
||||
/* Tags (workspace) */
|
||||
availTags: [], tagAdding: false, tagExistingOpen: false,
|
||||
newTagColor: '#787774',
|
||||
tagColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
|
||||
/* Icon picker */
|
||||
iconOpen: false,
|
||||
iconChoices: ['📄','📝','📕','📁','⭐','🔖','📌','✅','💡','🔥','🚀','🎯','📊','🗓️','🔗','🧩','📎','🎨','🐛','⚙️','❤️','👍','✨','🏷️','🗒️','📚'],
|
||||
|
||||
/* Positionne le menu à l'écran et expose les handlers de la page.
|
||||
⚠️ Nom : openMenu (et PAS open) — « open » est réservé au booléen
|
||||
d'état x-show. Une collision ici rendrait le menu PERMANENT. */
|
||||
openMenu(ev, node, pageHash, handlers) {
|
||||
handlers = handlers || {};
|
||||
this.x = Math.min((ev && ev.clientX) || 0, window.innerWidth - 230);
|
||||
this.y = Math.min((ev && ev.clientY) || 0, window.innerHeight - 60);
|
||||
this.node = node;
|
||||
this.page = pageHash;
|
||||
this.handlers = handlers;
|
||||
this.tagAdding = false;
|
||||
this.tagExistingOpen = false;
|
||||
this.iconOpen = false;
|
||||
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));
|
||||
this.y = Math.max(4, Math.min((ev && ev.clientY) || 0, window.innerHeight - 48));
|
||||
this.open = true;
|
||||
},
|
||||
close() { this.open = false; this.node = null; },
|
||||
close() {
|
||||
this.open = false;
|
||||
this.node = null;
|
||||
this.handlers = {};
|
||||
this.tagAdding = false;
|
||||
this.tagExistingOpen = false;
|
||||
this.iconOpen = false;
|
||||
},
|
||||
|
||||
/* Un item est-il disponible pour CETTE page ? (union vs capacités) */
|
||||
has(key) { return typeof this.handlers[key] === 'function'; },
|
||||
@@ -69,95 +77,196 @@
|
||||
if (fn) try { fn(this.node); } catch (e) { console.error('fdCtx.run', e); }
|
||||
this.close();
|
||||
},
|
||||
hop(key) { this.run(key); },
|
||||
|
||||
/* Routage Tags (workspace) — cache + ajout/suppression. */
|
||||
avail() { return this._cache.avail || []; },
|
||||
setAvail(a) { this._cache.avail = a || []; },
|
||||
removeTag(id) { this.run('tagRemove'); },
|
||||
/* ── Tags ── */
|
||||
avail() { return this.availTags || []; },
|
||||
setAvail(a) { this.availTags = a || []; },
|
||||
removeTag(id) {
|
||||
var fn = this.handlers.tagRemove;
|
||||
if (fn) try { fn(id); } catch (e) { console.error('fdCtx.removeTag', e); }
|
||||
},
|
||||
addExistingTag(t) {
|
||||
var fn = this.handlers.tagExisting;
|
||||
if (fn) try { fn(t); } catch (e) { console.error('fdCtx.addExistingTag', e); }
|
||||
this.tagExistingOpen = false;
|
||||
},
|
||||
addNewTag(name, color) {
|
||||
name = (name || '').trim();
|
||||
if (!name) return;
|
||||
var fn = this.handlers.tagAdd;
|
||||
if (fn) try { fn(name, color || this.newTagColor); } catch (e) { console.error('fdCtx.addNewTag', e); }
|
||||
this.tagAdding = false;
|
||||
},
|
||||
|
||||
/* ── Icon picker ── */
|
||||
setIcon(icon) {
|
||||
icon = (icon || '').trim();
|
||||
if (!icon) return;
|
||||
var fn = this.handlers.setIcon;
|
||||
if (fn) try { fn(icon); } catch (e) { console.error('fdCtx.setIcon', e); }
|
||||
this.close();
|
||||
},
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="more-menu" x-show="$store.fdCtx.open"
|
||||
@click.outside="$store.fdCtx.close()" x-transition
|
||||
<style>
|
||||
.fd-ctx-menu{position:fixed !important;top:0;left:0;min-width:230px;max-width:280px;max-height:calc(100vh - 16px);overflow-y:auto;z-index:2000;padding:4px;}
|
||||
.fd-ctx-menu .menu-item{display:flex;align-items:center;gap:8px;padding:6px 10px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:4px;transition:background .1s;white-space:nowrap;}
|
||||
.fd-ctx-menu .menu-item:hover{background:var(--bg-hover);}
|
||||
.fd-ctx-menu .menu-item svg{width:14px;height:14px;flex-shrink:0;}
|
||||
.fd-ctx-menu .menu-danger{color:var(--danger);}
|
||||
.fd-ctx-menu .menu-shortcut{margin-left:auto;font-size:11px;color:var(--text-dim);padding-left:16px;}
|
||||
.fd-ctx-menu .more-sep{height:1px;background:var(--border);margin:4px 6px;}
|
||||
.fd-ctx-menu .ctx-label{color:var(--text-dim);font-size:11px;cursor:default;text-transform:uppercase;letter-spacing:.04em;}
|
||||
.fd-ctx-menu .ctx-label:hover{background:transparent;}
|
||||
.fd-ctx-menu .ctx-empty{color:var(--text-dim);font-size:11px;cursor:default;}
|
||||
.fd-ctx-menu .ctx-empty:hover{background:transparent;}
|
||||
.fd-ctx-menu .ctx-caret{font-size:10px;color:var(--text-dim);}
|
||||
.fd-ctx-menu .ctx-x{color:var(--text-dim);font-size:14px;line-height:1;padding:0 2px;}
|
||||
.fd-ctx-menu .ctx-x:hover{color:var(--danger);}
|
||||
.fd-ctx-menu .ctx-sub{padding:2px 0 4px;background:var(--bg-tertiary);border-radius:6px;margin:2px 6px;}
|
||||
.fd-ctx-menu .ctx-tag-color{width:10px;height:10px;border-radius:3px;display:inline-block;flex-shrink:0;}
|
||||
.fd-ctx-menu .ctx-colors{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;padding:2px 4px 6px;}
|
||||
.fd-ctx-menu .ctx-swatch{width:16px;height:16px;border-radius:4px;cursor:pointer;border:2px solid transparent;}
|
||||
.fd-ctx-menu .ctx-swatch.selected{border-color:var(--text-primary);}
|
||||
.fd-ctx-menu .ctx-input{flex:1;min-width:0;font-size:12px;padding:3px 6px;background:var(--bg-primary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);outline:none;}
|
||||
.fd-ctx-menu .ctx-input:focus{border-color:var(--accent);}
|
||||
.fd-ctx-menu .ctx-btn{padding:3px 10px;font-size:12px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;}
|
||||
.fd-ctx-menu .ctx-icons{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;padding:2px 4px 4px;}
|
||||
.fd-ctx-menu .ctx-icon{width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:15px;background:transparent;border:none;border-radius:4px;cursor:pointer;}
|
||||
.fd-ctx-menu .ctx-icon:hover{background:var(--bg-hover);}
|
||||
</style>
|
||||
|
||||
<div class="more-menu fd-ctx-menu" x-show="$store.fdCtx.open" x-cloak
|
||||
@click.outside="$store.fdCtx.close()"
|
||||
@keydown.escape.window="$store.fdCtx.close()"
|
||||
x-transition
|
||||
:style="{ top: $store.fdCtx.y + 'px', left: $store.fdCtx.x + 'px' }">
|
||||
|
||||
{# ── 1. Open ── #}
|
||||
<div class="menu-item" @click="$store.fdCtx.run('open')">
|
||||
{# ── Open ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('open')" @click="$store.fdCtx.run('open')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
Open
|
||||
<span style="flex:1;">Open</span>
|
||||
</div>
|
||||
|
||||
{# ── 2. Open in side peek (library) ── #}
|
||||
{# ── Open in new tab ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('openTab')" @click="$store.fdCtx.run('openTab')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
<span style="flex:1;">Open in new tab</span><span class="menu-shortcut">Ctrl+⇧+↩</span>
|
||||
</div>
|
||||
|
||||
{# ── Open in side peek ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('peek')" @click="$store.fdCtx.run('peek')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="13" y1="3" x2="13" y2="21"/></svg>
|
||||
Open in side peek
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
||||
<span style="flex:1;">Open in side peek</span><span class="menu-shortcut">Alt+Click</span>
|
||||
</div>
|
||||
|
||||
{# ── 3. Open folder (workspace, dossiers) ── #}
|
||||
{# ── Open folder (workspace, dossiers) ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('folder')" @click="$store.fdCtx.run('folder')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||||
Open folder
|
||||
<span style="flex:1;">Open folder</span>
|
||||
</div>
|
||||
|
||||
{# ── 4. Rename ── #}
|
||||
<div class="menu-item" @click="$store.fdCtx.run('rename')">
|
||||
<div class="more-sep" x-show="$store.fdCtx.has('rename') || $store.fdCtx.has('setIcon') || $store.fdCtx.has('duplicate') || $store.fdCtx.has('link') || $store.fdCtx.has('move')"></div>
|
||||
|
||||
{# ── Rename ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('rename')" @click="$store.fdCtx.run('rename')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4z"/></svg>
|
||||
Rename
|
||||
<span style="flex:1;">Rename</span><span class="menu-shortcut">Ctrl+⇧+R</span>
|
||||
</div>
|
||||
|
||||
{# ── 5. Add/Remove Favorites (library) ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('fav')" @click="$store.fdCtx.run('fav')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
<span x-text="$store.fdCtx.node && $store.fdCtx.node.favorited ? 'Remove from Favorites' : 'Add to Favorites'"></span>
|
||||
{# ── Edit icon ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('setIcon')" @click.stop="$store.fdCtx.iconOpen = !$store.fdCtx.iconOpen">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
|
||||
<span style="flex:1;">Edit icon</span><span class="ctx-caret" x-text="$store.fdCtx.iconOpen ? '▾' : '▸'"></span>
|
||||
</div>
|
||||
<div x-show="$store.fdCtx.iconOpen" class="ctx-sub">
|
||||
<div class="ctx-icons">
|
||||
<template x-for="ic in $store.fdCtx.iconChoices" :key="'ic'+ic">
|
||||
<button type="button" class="ctx-icon" @click.stop="$store.fdCtx.setIcon(ic)" x-text="ic"></button>
|
||||
</template>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;padding:0 4px;">
|
||||
<input class="ctx-input" x-ref="ctxIconInput" placeholder="Custom emoji…" @click.stop @keydown.enter.prevent="$store.fdCtx.setIcon($refs.ctxIconInput.value)">
|
||||
<button type="button" class="ctx-btn" @click.stop="$store.fdCtx.setIcon($refs.ctxIconInput.value)">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── 6. Duplicate ── #}
|
||||
<div class="menu-item" @click="$store.fdCtx.run('duplicate')">
|
||||
{# ── Duplicate ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('duplicate')" @click="$store.fdCtx.run('duplicate')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
|
||||
Duplicate
|
||||
<span style="flex:1;">Duplicate</span>
|
||||
</div>
|
||||
|
||||
{# ── 7. Copy link (library) ── #}
|
||||
{# ── Copy link ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('link')" @click="$store.fdCtx.run('link')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
||||
Copy link
|
||||
<span style="flex:1;">Copy link</span>
|
||||
</div>
|
||||
|
||||
{# ── 8. Move to (library) ── #}
|
||||
{# ── Move to ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('move')" @click="$store.fdCtx.run('move')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||||
Move to
|
||||
<span style="flex:1;">Move to</span><span class="menu-shortcut">Ctrl+⇧+P</span>
|
||||
</div>
|
||||
|
||||
{# ── 9. Tags (workspace) ── #}
|
||||
<div class="more-sep" x-show="$store.fdCtx.has('tagExisting') || $store.fdCtx.has('tagNew')"></div>
|
||||
<template x-if="$store.fdCtx.has('tagExisting') || $store.fdCtx.has('tagNew')">
|
||||
<div style="padding:2px 0;">
|
||||
<div class="menu-item" @click="$store.fdCtx.handlers.tagExisting" style="cursor:default;">
|
||||
<span style="flex:1;">Tags</span>
|
||||
{# ── Favorites ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('fav')" @click="$store.fdCtx.run('fav')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
<span style="flex:1;" x-text="$store.fdCtx.node && $store.fdCtx.node.favorited ? 'Remove from Favorites' : 'Add to Favorites'"></span>
|
||||
</div>
|
||||
|
||||
{# ── Remove from Recents ── #}
|
||||
<div class="menu-item" x-show="$store.fdCtx.has('recent')" @click="$store.fdCtx.run('recent')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
||||
<span style="flex:1;">Remove from Recents</span>
|
||||
</div>
|
||||
|
||||
{# ── Tags (workspace) ── #}
|
||||
<div class="more-sep" x-show="$store.fdCtx.has('tagRemove') || $store.fdCtx.has('tagAdd')"></div>
|
||||
<div x-show="$store.fdCtx.has('tagRemove') || $store.fdCtx.has('tagAdd')">
|
||||
<div class="menu-item ctx-label"><span style="flex:1;">Tags</span></div>
|
||||
<template x-for="t in ($store.fdCtx.node && $store.fdCtx.node.tags) || []" :key="'ct'+t.id">
|
||||
<div class="menu-item" @click.stop="$store.fdCtx.removeTag(t.id)">
|
||||
<span style="display:flex;align-items:center;gap:8px;flex:1;overflow:hidden;"><span class="ctx-tag-color" :style="{ background: t.color }"></span><span x-text="t.name" style="overflow:hidden;text-overflow:ellipsis;"></span></span>
|
||||
<span class="ctx-x" title="Remove tag">×</span>
|
||||
</div>
|
||||
<template x-for="t in ($store.fdCtx.node && $store.fdCtx.node.tags) || []" :key="'ctx-tag' + t.id">
|
||||
<div class="menu-item" @click.stop="$store.fdCtx.has('tagRemove') && $store.fdCtx.handlers.tagRemove(t.id)">
|
||||
</template>
|
||||
<div class="menu-item ctx-empty" x-show="!($store.fdCtx.node && ($store.fdCtx.node.tags||[]).length)">No tags</div>
|
||||
<div class="menu-item" x-show="$store.fdCtx.avail().length" @click.stop="$store.fdCtx.tagExistingOpen = !$store.fdCtx.tagExistingOpen">
|
||||
<span style="flex:1;">Add existing tag</span><span class="ctx-caret" x-text="$store.fdCtx.tagExistingOpen ? '▾' : '▸'"></span>
|
||||
</div>
|
||||
<div x-show="$store.fdCtx.tagExistingOpen" class="ctx-sub">
|
||||
<template x-for="t in $store.fdCtx.avail()" :key="'ca'+t.id">
|
||||
<div class="menu-item" @click.stop="$store.fdCtx.addExistingTag(t)">
|
||||
<span style="display:flex;align-items:center;gap:8px;"><span class="ctx-tag-color" :style="{ background: t.color }"></span><span x-text="t.name"></span></span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="menu-item" x-show="!($store.fdCtx.node && ($store.fdCtx.node.tags||[]).length)" style="color:var(--text-dim);font-size:11px;cursor:default;"> No tags </div>
|
||||
<div class="menu-item" @click="$store.fdCtx.handlers.tagExisting">
|
||||
<span style="flex:1;">Add existing tag</span><span style="font-size:10px;">▸</span>
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="$store.fdCtx.tagAdding = !$store.fdCtx.tagAdding">
|
||||
<span style="flex:1;">+ New tag</span><span class="ctx-caret" x-text="$store.fdCtx.tagAdding ? '▾' : '▸'"></span>
|
||||
</div>
|
||||
<div x-show="$store.fdCtx.tagAdding" class="ctx-sub">
|
||||
<div class="ctx-colors">
|
||||
<template x-for="c in $store.fdCtx.tagColors" :key="'cc'+c">
|
||||
<span class="ctx-swatch" :class="{ selected: $store.fdCtx.newTagColor === c }" :style="{ background: c }" @click.stop="$store.fdCtx.newTagColor = c"></span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="menu-item" @click="$store.fdCtx.handlers.tagNew">
|
||||
<span style="flex:1;">New tag</span>
|
||||
<div style="display:flex;gap:4px;padding:0 4px;">
|
||||
<input class="ctx-input" x-ref="ctxTagInput" placeholder="Tag name…" @click.stop
|
||||
@keydown.enter.prevent="$store.fdCtx.addNewTag($event.target.value, $store.fdCtx.newTagColor); $event.target.value = ''"
|
||||
@keydown.escape.stop="$store.fdCtx.tagAdding = false">
|
||||
<button type="button" class="ctx-btn" @click.stop="$store.fdCtx.addNewTag($refs.ctxTagInput.value, $store.fdCtx.newTagColor); $refs.ctxTagInput.value = ''">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{# ── 10. Delete (danger) ── #}
|
||||
{# ── Delete (danger) ── #}
|
||||
<div class="more-sep"></div>
|
||||
<div class="menu-item menu-danger" @click="$store.fdCtx.run('delete')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
|
||||
Delete
|
||||
<span style="flex:1;">Move to Trash</span><span class="menu-shortcut">Del</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+52
-27
@@ -364,8 +364,6 @@
|
||||
</div>
|
||||
|
||||
{% include "_ctx_menu.html" %}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ── Move-to modal ── -->
|
||||
<div class="move-modal-overlay" x-show="moveModalOpen" @click.self="moveModalOpen=false" x-transition>
|
||||
@@ -475,11 +473,6 @@ function libraryPage() {
|
||||
// More menu
|
||||
moreOpen: false,
|
||||
|
||||
// Row context menu
|
||||
ctxMenuItem: null,
|
||||
ctxMenuX: 0,
|
||||
ctxMenuY: 0,
|
||||
|
||||
// Move-to modal
|
||||
moveModalOpen: false,
|
||||
moveIds: [],
|
||||
@@ -598,8 +591,10 @@ function libraryPage() {
|
||||
var name = icon || fallback || 'file';
|
||||
// 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) name = fallback || 'file';
|
||||
return getSvgIcon(name, 14);
|
||||
if (valid.indexOf(name) !== -1) return getSvgIcon(name, 14);
|
||||
// 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>';
|
||||
return getSvgIcon(fallback || 'file', 14);
|
||||
},
|
||||
|
||||
_recomputeFlatItems() {
|
||||
@@ -673,27 +668,25 @@ function libraryPage() {
|
||||
evt.preventDefault();
|
||||
var item = this._findItem(id);
|
||||
if (!item) return;
|
||||
if (!(window.Alpine && Alpine.store('fdCtx'))) return;
|
||||
/* MENU UNIFIÉ (partial _ctx_menu.html + store fdCtx) — handlers library
|
||||
complète l'union (8 items). Open folder/tags non prévus côté library :
|
||||
complète l'union. Open folder/tags non prévus côté library :
|
||||
fdCtx les cache automatiquement via handlers absents. */
|
||||
var handlers = {
|
||||
open: function (n) { libraryPage().openItem(n); },
|
||||
peek: function (n) { libraryPage().openPeek(n); },
|
||||
rename: function (n) { libraryPage().startRename(n); },
|
||||
fav: function (n) { libraryPage().toggleFavoriteItem(n); },
|
||||
duplicate: function (n) { libraryPage().duplicateItem(n); },
|
||||
link: function (n) { libraryPage().copyLink(n); },
|
||||
move: function (n) { libraryPage().openMovePicker([n.id]); },
|
||||
del: function (n) { libraryPage().deleteItem(n); }
|
||||
};
|
||||
var self = this;
|
||||
if (window.Alpine && Alpine.store('fdCtx')) {
|
||||
Alpine.store('fdCtx').open(evt, item, 'library', handlers);
|
||||
Alpine.store('fdCtx').closeMenu = function () { ctxMenuItem = null; };
|
||||
}
|
||||
this.ctxMenuItem = item parallels ?? null;
|
||||
this.ctxMenuX = Math.min(evt.clientX, window.innerWidth - 220);
|
||||
this.ctxMenuY = Math.min(evt.clientY, window.innerHeight - 320);
|
||||
var handlers = {
|
||||
open: function (n) { self.openItem(n); },
|
||||
openTab: function (n) { self.openInNewTab(n); },
|
||||
peek: function (n) { self.openPeek(n); },
|
||||
rename: function (n) { self.startRename(n); },
|
||||
setIcon: function (icon) { self.setItemIcon(item, icon); },
|
||||
duplicate: function (n) { self.duplicateItem(n); },
|
||||
link: function (n) { self.copyLink(n); },
|
||||
move: function (n) { self.openMovePicker([n.id]); },
|
||||
fav: function (n) { self.toggleFavoriteItem(n); },
|
||||
delete: function (n) { self.deleteItem(n); }
|
||||
};
|
||||
if (this.tab === 'recents') handlers.recent = function (n) { self.removeItemFromRecents(n); };
|
||||
Alpine.store('fdCtx').openMenu(evt, item, 'library', handlers);
|
||||
},
|
||||
|
||||
// ── Drag & drop (move / reparent) ──
|
||||
@@ -1058,6 +1051,38 @@ function libraryPage() {
|
||||
if (window.showToast) window.showToast('Link copied', 'success');
|
||||
},
|
||||
|
||||
openInNewTab(item) {
|
||||
if (!item || !item.id) return;
|
||||
window.open('/pages/' + item.id, '_blank');
|
||||
},
|
||||
|
||||
removeItemFromRecents(item) {
|
||||
if (!item) return;
|
||||
var id = item.id;
|
||||
this.items = this.items.filter(function(it) { return it.id !== id; });
|
||||
this._recomputeFlatItems();
|
||||
if (window.showToast) window.showToast('Removed from Recents', 'success');
|
||||
},
|
||||
|
||||
async setItemIcon(item, icon) {
|
||||
if (!item || !item.id) return;
|
||||
icon = (icon || '').trim();
|
||||
if (!icon) return;
|
||||
try {
|
||||
var r = await fetch('/board/api/pages/' + item.id + '/icon', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': this._getCsrf()},
|
||||
body: JSON.stringify({icon: icon})
|
||||
});
|
||||
if (!r.ok) throw new Error('icon update failed');
|
||||
item.icon = icon;
|
||||
this._renderTable();
|
||||
if (window.showToast) window.showToast('Icon updated', 'success');
|
||||
} catch(e) {
|
||||
if (window.showToast) window.showToast('Failed to update icon', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async toggleFavoriteItem(item) {
|
||||
if (!item) return;
|
||||
var csrf = this._getCsrf();
|
||||
|
||||
+115
-153
@@ -526,13 +526,7 @@ window._wsInitData = (function() {
|
||||
viewIcons: {tree:'',list:'',details:'',title:'',content:''},
|
||||
viewLabels: {tree:'Tree',list:'List',details:'Details',title:'Cards',content:'Content'},
|
||||
|
||||
// Context menu
|
||||
ctxMenu: {visible:false,x:0,y:0,node:null,tags:[]},
|
||||
ctxTagAdding: false,
|
||||
ctxTagExistingOpen: false,
|
||||
ctxAvailTags: [],
|
||||
ctxNewTagColor: '#787774',
|
||||
ctxTagColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
|
||||
// Context menu — state lives in the shared fdCtx Alpine store (_ctx_menu.html)
|
||||
|
||||
// Tags
|
||||
workspaceTags: [],
|
||||
@@ -869,7 +863,7 @@ window._wsInitData = (function() {
|
||||
selectForPreview(node) {
|
||||
var panel = document.getElementById('ws-preview-panel');
|
||||
if (!panel) return;
|
||||
document.getElementById('ws-preview-icon').innerHTML = this._fileIcon(node.name, node.is_folder, node.content_format);
|
||||
document.getElementById('ws-preview-icon').innerHTML = this._fileIcon(node.name, node.is_folder, node.content_format, node.page_icon);
|
||||
document.getElementById('ws-preview-title').textContent = node.name;
|
||||
var openBtn = document.getElementById('ws-preview-open-btn');
|
||||
openBtn.onclick = function() { window.location = '/pages/' + node.id; };
|
||||
@@ -1008,61 +1002,95 @@ window._wsInitData = (function() {
|
||||
// ── Content snippet for content view ──
|
||||
contentSnippetMap: {},
|
||||
|
||||
// ── Context menu ──
|
||||
// ── Context menu (menu unifié partagé via le store Alpine fdCtx) ──
|
||||
onContextMenu(ev) {
|
||||
var el = ev.target.closest('[data-ws-id]');
|
||||
var menu = document.querySelector('.more-menu');
|
||||
if (!el || !menu) { if (menu) menu.style.display = 'none'; return; }
|
||||
if (!el) return;
|
||||
var id = parseInt(el.getAttribute('data-ws-id'));
|
||||
if (!id) { menu.style.display = 'none'; return; }
|
||||
var node = this._findNodeById(this.displayTree, id);
|
||||
if (!node) { menu.style.display = 'none'; return; }
|
||||
// Ensure workspace tags are loaded
|
||||
if (!this.workspaceTags || this.workspaceTags.length === 0) {
|
||||
this.loadWorkspaceTags();
|
||||
}
|
||||
// Store in Alpine data for menu actions (ctxMenuOpenPage, etc.)
|
||||
this.ctxMenu = {visible:true, x:ev.clientX, y:ev.clientY, node:node, tags:[...(node.tags||[])]};
|
||||
this.ctxTagAdding = false;
|
||||
this.ctxTagExistingOpen = false;
|
||||
this.ctxNewTagColor = '#787774';
|
||||
this.ctxAvailTags = (this.workspaceTags || []).filter(function(t) {
|
||||
return !(node.tags||[]).some(function(nt) { return nt.id === t.id; });
|
||||
if (!id) return;
|
||||
var node = this._findNodeById(this.displayTree, id) || this._findNodeById(this.tree, id);
|
||||
if (!node) return;
|
||||
var store = window.Alpine && Alpine.store('fdCtx');
|
||||
if (!store) return;
|
||||
var self = this;
|
||||
if (!this.workspaceTags || this.workspaceTags.length === 0) this.loadWorkspaceTags();
|
||||
store.setAvail(this._ctxAvailTags(node));
|
||||
var handlers = {
|
||||
open: function (n) { self.openPage(n.id); },
|
||||
openTab: function (n) { window.open('/pages/' + n.id, '_blank'); },
|
||||
peek: function (n) { self.openSidePeek(n.id); },
|
||||
folder: function (n) { if (n.is_folder) self.navigateToFolder(n.id); },
|
||||
rename: function (n) { self.startRename(n); },
|
||||
setIcon: function (icon) { self.setItemIcon(node, icon); },
|
||||
duplicate: function (n) { self.duplicateItem(n); },
|
||||
link: function (n) { self.copyItemLink(n); },
|
||||
move: function (n) { self._ctxMove(n); },
|
||||
fav: function (n) { self.toggleFavorite(n); },
|
||||
delete: function (n) { self.deleteWithUndo(n); },
|
||||
tagExisting: function (t) { self.ctxAddExistingTag(t); },
|
||||
tagAdd: function (name, color) { self.ctxAddNewTag(name, color); },
|
||||
tagRemove: function (tagId) { self.ctxRemoveTag(tagId); }
|
||||
};
|
||||
store.openMenu(ev, node, 'workspace', handlers);
|
||||
},
|
||||
|
||||
_ctxAvailTags(node) {
|
||||
return (this.workspaceTags || []).filter(function(t) {
|
||||
return !((node && node.tags) || []).some(function(nt) { return nt.id === t.id; });
|
||||
});
|
||||
// DOM manipulation (bypasses Alpine reactivity issues)
|
||||
menu.style.display = 'block';
|
||||
menu.style.left = ev.clientX + 'px';
|
||||
menu.style.top = ev.clientY + 'px';
|
||||
// Reposition if offscreen
|
||||
var rect = menu.getBoundingClientRect();
|
||||
var x = ev.clientX, y = ev.clientY;
|
||||
if (x + rect.width > window.innerWidth - 4) x = window.innerWidth - rect.width - 4;
|
||||
if (y + rect.height > window.innerHeight - 4) y = window.innerHeight - rect.height - 4;
|
||||
if (x < 4) x = 4;
|
||||
if (y < 4) y = 4;
|
||||
menu.style.left = x + 'px';
|
||||
menu.style.top = y + 'px';
|
||||
this.ctxMenu.x = x;
|
||||
this.ctxMenu.y = y;
|
||||
// Sync to _ctxMenuData for Alpine reactivity
|
||||
var c = window._ctxMenuData;
|
||||
c.ctxMenu = this.ctxMenu;
|
||||
c.ctxTagAdding = this.ctxTagAdding;
|
||||
c.ctxTagExistingOpen = this.ctxTagExistingOpen;
|
||||
c.ctxAvailTags = this.ctxAvailTags;
|
||||
c.ctxNewTagColor = this.ctxNewTagColor;
|
||||
// Also force Alpine update on the menu element
|
||||
var menuEl = document.querySelector('.more-menu');
|
||||
if (menuEl && menuEl._x_dataStack && menuEl._x_dataStack[0]) {
|
||||
var d = menuEl._x_dataStack[0];
|
||||
d.ctxMenu = this.ctxMenu;
|
||||
d.ctxTagAdding = this.ctxTagAdding;
|
||||
d.ctxTagExistingOpen = this.ctxTagExistingOpen;
|
||||
d.ctxAvailTags = [...this.ctxAvailTags];
|
||||
d.ctxNewTagColor = this.ctxNewTagColor;
|
||||
},
|
||||
|
||||
_ctxRefreshAvail() {
|
||||
var store = window.Alpine && Alpine.store('fdCtx');
|
||||
if (store && store.node) store.setAvail(this._ctxAvailTags(store.node));
|
||||
},
|
||||
|
||||
copyItemLink(node) {
|
||||
if (!node) return;
|
||||
navigator.clipboard.writeText(window.location.origin + '/pages/' + node.id);
|
||||
if (window.showToast) window.showToast('Link copied', 'success');
|
||||
},
|
||||
|
||||
async toggleFavorite(node) {
|
||||
if (!node) return;
|
||||
try {
|
||||
var method = node.favorited ? 'DELETE' : 'POST';
|
||||
var r = await fetch('/board/api/favorites/' + node.id, { method: method });
|
||||
if (r.ok) {
|
||||
node.favorited = !node.favorited;
|
||||
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
|
||||
if (window.showToast) window.showToast(node.favorited ? 'Added to Favorites' : 'Removed from Favorites', 'success');
|
||||
}
|
||||
} catch(e) {}
|
||||
},
|
||||
|
||||
async setItemIcon(node, icon) {
|
||||
if (!node || !node.id) return;
|
||||
icon = (icon || '').trim();
|
||||
if (!icon) return;
|
||||
try {
|
||||
var r = await fetch('/board/api/pages/' + node.id + '/icon', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({icon: icon})
|
||||
});
|
||||
if (!r.ok) throw new Error('icon update failed');
|
||||
node.page_icon = icon;
|
||||
this.displayTree = [...this.tree];
|
||||
this._reloadAfterAction();
|
||||
if (window.showToast) window.showToast('Icon updated', 'success');
|
||||
} catch(e) {
|
||||
if (window.showToast) window.showToast('Failed to update icon', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
_ctxMove(node) {
|
||||
if (!node) return;
|
||||
this.selectedIds = {};
|
||||
this.selectedIds[node.id] = true;
|
||||
this.openMovePicker();
|
||||
},
|
||||
|
||||
// ── Long-press (mobile context menu) ──
|
||||
_touchStartX: 0,
|
||||
_touchStartY: 0,
|
||||
@@ -1107,84 +1135,37 @@ window._wsInitData = (function() {
|
||||
this.ctxAddNewTag(tag.name, tag.color);
|
||||
},
|
||||
|
||||
ctxMenuOpenPage() {
|
||||
if (this.ctxMenu.node) this.openPage(this.ctxMenu.node.id);
|
||||
document.querySelector('.more-menu').style.display = 'none';
|
||||
},
|
||||
|
||||
ctxMenuOpenFolder() {
|
||||
if (this.ctxMenu.node) this.navigateToFolder(this.ctxMenu.node.id);
|
||||
document.querySelector('.more-menu').style.display = 'none';
|
||||
},
|
||||
|
||||
ctxMenuRename() {
|
||||
if (this.ctxMenu.node) { this.startRename(this.ctxMenu.node); document.querySelector('.more-menu').style.display = 'none'; }
|
||||
},
|
||||
|
||||
ctxMenuDuplicate() {
|
||||
if (this.ctxMenu.node) { this.duplicateItem(this.ctxMenu.node); document.querySelector('.more-menu').style.display = 'none'; }
|
||||
},
|
||||
|
||||
ctxMenuDelete() {
|
||||
if (this.ctxMenu.node) { this.deleteWithUndo(this.ctxMenu.node); document.querySelector('.more-menu').style.display = 'none'; }
|
||||
},
|
||||
|
||||
ctxRemoveTag(tagId) {
|
||||
var node = this.ctxMenu.node;
|
||||
var store = window.Alpine && Alpine.store('fdCtx');
|
||||
var node = store && store.node;
|
||||
if (!node) return;
|
||||
this.removeTag(node.id, tagId);
|
||||
this.ctxMenu.tags = this.ctxMenu.tags.filter(function(t) { return t.id !== tagId; });
|
||||
// Refresh available tags
|
||||
this.ctxAvailTags = (this.workspaceTags || []).filter(function(t) {
|
||||
return !(this.ctxMenu.tags||[]).some(function(nt) { return nt.id === t.id; });
|
||||
}.bind(this));
|
||||
// Sync to _ctxMenuData
|
||||
window._ctxMenuData.ctxAvailTags = this.ctxAvailTags;
|
||||
window._ctxMenuData.ctxMenu = this.ctxMenu;
|
||||
// Also force Alpine update
|
||||
var menuEl = document.querySelector('.more-menu');
|
||||
if (menuEl && menuEl._x_dataStack && menuEl._x_dataStack[0]) {
|
||||
menuEl._x_dataStack[0].ctxAvailTags = [...this.ctxAvailTags];
|
||||
menuEl._x_dataStack[0].ctxMenu = this.ctxMenu;
|
||||
}
|
||||
node.tags = (node.tags || []).filter(function(t) { return t.id !== tagId; });
|
||||
this._ctxRefreshAvail();
|
||||
},
|
||||
|
||||
async ctxAddNewTag(tagName, color) {
|
||||
tagName = (tagName || '').trim();
|
||||
if (!tagName) return;
|
||||
var node = this.ctxMenu.node;
|
||||
var store = window.Alpine && Alpine.store('fdCtx');
|
||||
var node = store && store.node;
|
||||
if (!node) return;
|
||||
color = color || window._ctxMenuData.ctxNewTagColor;
|
||||
var r = await fetch('/api/local-workspace/items/' + node.id + '/tags', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({name: tagName.trim(), color: color})
|
||||
});
|
||||
if (r.ok) {
|
||||
var d = await r.json();
|
||||
this.ctxMenu.tags.push(d.tag);
|
||||
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
|
||||
this.displayTree = [...this.tree];
|
||||
this.loadWorkspaceTags();
|
||||
// Refresh available tags
|
||||
var self = this;
|
||||
setTimeout(function() { self.loadWorkspaceTags().then(function() {
|
||||
self.ctxAvailTags = (self.workspaceTags || []).filter(function(t) {
|
||||
return !(self.ctxMenu.tags||[]).some(function(nt) { return nt.id === t.id; });
|
||||
});
|
||||
// Sync to _ctxMenuData
|
||||
window._ctxMenuData.ctxAvailTags = self.ctxAvailTags;
|
||||
window._ctxMenuData.ctxMenu = self.ctxMenu;
|
||||
// Also force Alpine update
|
||||
var menuEl = document.querySelector('.more-menu');
|
||||
if (menuEl && menuEl._x_dataStack && menuEl._x_dataStack[0]) {
|
||||
menuEl._x_dataStack[0].ctxAvailTags = [...self.ctxAvailTags];
|
||||
menuEl._x_dataStack[0].ctxMenu = self.ctxMenu;
|
||||
}
|
||||
// Close menu after successful add
|
||||
document.querySelector('.more-menu').style.display = 'none';
|
||||
}); }, 200);
|
||||
}
|
||||
this.ctxTagAdding = false;
|
||||
color = color || (store && store.newTagColor) || '#787774';
|
||||
try {
|
||||
var r = await fetch('/api/local-workspace/items/' + node.id + '/tags', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({name: tagName, color: color})
|
||||
});
|
||||
if (r.ok) {
|
||||
var d = await r.json();
|
||||
if (!node.tags) node.tags = [];
|
||||
if (!node.tags.some(function(t) { return t.id === d.tag.id; })) node.tags.push(d.tag);
|
||||
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
|
||||
this.displayTree = [...this.tree];
|
||||
this.loadWorkspaceTags();
|
||||
this._ctxRefreshAvail();
|
||||
}
|
||||
} catch(e) {}
|
||||
},
|
||||
|
||||
_markTree(nodes, depth) {
|
||||
@@ -1205,7 +1186,7 @@ window._wsInitData = (function() {
|
||||
for (var i=0; i<children.length; i++) {
|
||||
var c = children[i];
|
||||
var hasKids = c.children && c.children.length > 0;
|
||||
var icon = this._fileIcon(c.name, c.is_folder, c.content_format);
|
||||
var icon = this._fileIcon(c.name, c.is_folder, c.content_format, c.page_icon);
|
||||
var escName = this.esc(c.name);
|
||||
var escId = c.id;
|
||||
html += '<li><div class="ws-tree-item" style="padding-left:'+(depth*20)+'px" draggable="true"'
|
||||
@@ -1759,7 +1740,8 @@ window._wsInitData = (function() {
|
||||
},
|
||||
|
||||
// ── File type icon ──
|
||||
_fileIcon(name, isFolder, contentFormat) {
|
||||
_fileIcon(name, isFolder, contentFormat, customIcon) {
|
||||
if (customIcon) return '<span class="page-icon-svg" style="font-size:14px;line-height:1;">'+this.esc(customIcon)+'</span>';
|
||||
if (isFolder) return '<span class="page-icon-svg">'+getSvgIcon('folder',14)+'</span>';
|
||||
// FlowDeck internal pages (no extension, blocks/markdown format)
|
||||
if (contentFormat && contentFormat !== 'file') return '<span class="page-icon-svg">'+getSvgIcon('edit',14)+'</span>';
|
||||
@@ -2428,32 +2410,12 @@ for (var _i = 0; _i < _wsKeys.length; _i++) {
|
||||
try { window[_wsKeys[_i]] = window._wsInitData[_wsKeys[_i]]; } catch(e) {}
|
||||
}
|
||||
_wsInitData = window._wsInitData;
|
||||
// Objet léger pour le ctx-menu (évite le conflit $nextTick avec Alpine)
|
||||
window._ctxMenuData = {
|
||||
ctxMenu: {visible:false,x:0,y:0,node:null,tags:[]},
|
||||
ctxTagAdding: false,
|
||||
ctxTagExistingOpen: false,
|
||||
ctxAvailTags: [],
|
||||
ctxNewTagColor: '#787774',
|
||||
ctxTagColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
|
||||
// Use Alpine proxy (window._wsData) for reactivity, not raw object (window._wsInitData)
|
||||
_ws() { return window._wsData || window._wsInitData; },
|
||||
ctxMenuOpenPage() { this._ws().ctxMenuOpenPage(); },
|
||||
ctxMenuOpenFolder() { this._ws().ctxMenuOpenFolder(); },
|
||||
ctxMenuRename() { this._ws().ctxMenuRename(); },
|
||||
ctxMenuDuplicate() { this._ws().ctxMenuDuplicate(); },
|
||||
ctxMenuDelete() { this._ws().ctxMenuDelete(); },
|
||||
ctxAddExistingTag(t) { this._ws().ctxAddExistingTag(t); },
|
||||
ctxAddNewTag(n, c) { this._ws().ctxAddNewTag(n, c); },
|
||||
ctxRemoveTag(id) { this._ws().ctxRemoveTag(id); },
|
||||
};
|
||||
console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(window._wsInitData).length);
|
||||
</script>
|
||||
<script>console.log('FLOWDECK v2.4.6 — ws-split about to render, _wsInitData exists:', typeof window._wsInitData !== 'undefined');</script>
|
||||
|
||||
<div class="ws-split"
|
||||
@contextmenu.prevent="_wsInitData.onContextMenu($event)"
|
||||
@click.self="ctxMenu.visible = false"
|
||||
@touchstart="onTouchStart($event)"
|
||||
@touchend="onTouchEnd($event)"
|
||||
@touchmove="onTouchMove($event)"
|
||||
@@ -2678,7 +2640,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
<span style="width:16px;flex-shrink:0;"></span>
|
||||
</template>
|
||||
|
||||
<span class="icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
|
||||
<span class="icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
|
||||
<template x-if="node.is_folder">
|
||||
<span class="name folder" @click.stop="navigateToFolder(node.id)"
|
||||
@@ -2776,7 +2738,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
<tr :class="{ selected: !!selectedIds[node.id] }">
|
||||
<td><input type="checkbox" :checked="!!selectedIds[node.id]" @click.stop="toggleSelect(node, $event)"></td>
|
||||
<td>
|
||||
<span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
|
||||
<span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span class="col-name" @click="node.is_folder ? navigateToFolder(node.id) : openPage(node.id)" x-text="node.name"></span>
|
||||
</td>
|
||||
@@ -2833,7 +2795,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
</tr>
|
||||
<template x-for="node in displayTree" :key="'l'+node.id">
|
||||
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer">
|
||||
<td><span x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
|
||||
<td><span x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span></td>
|
||||
<td class="col-name" x-text="node.name"></td>
|
||||
<td style="font-size:12px;color:var(--text-tertiary)" x-text="node.is_folder ? 'Folder' : _fileTypeLabel(node.name, node.content_format)"></td>
|
||||
@@ -2871,7 +2833,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
<template x-for="node in displayTree" :key="'d'+node.id">
|
||||
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer"
|
||||
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
|
||||
<td><span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
|
||||
<td><span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span></td>
|
||||
<td class="col-name" x-text="node.name"></td>
|
||||
<td class="col-path" x-text="node._path || '—'"></td>
|
||||
@@ -2902,7 +2864,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
<template x-for="node in displayTree" :key="'g'+node.id">
|
||||
<div class="title-card" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
|
||||
:class="{ 'selected-card': previewItem && previewItem.id === node.id }">
|
||||
<div class="title-card-icon" x-html="node.is_folder ? getSvgIcon('folder',24) : _fileIcon(node.name, node.is_folder, node.content_format)"></div>
|
||||
<div class="title-card-icon" x-html="node.is_folder ? getSvgIcon('folder',24) : _fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></div>
|
||||
<div class="title-card-name">
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span x-text="node.name"></span>
|
||||
@@ -2929,7 +2891,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
<div class="content-item" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
|
||||
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
|
||||
<div class="content-item-header">
|
||||
<span x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
|
||||
<span x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span class="col-name" x-text="node.name"></span>
|
||||
<span style="font-size:11px;color:var(--text-tertiary);margin-left:auto" x-text="_formatDate(node.updated_at)"></span>
|
||||
|
||||
Reference in New Issue
Block a user