Config:
- Fixed SQLite path parsing on Windows (handle drive letters without prepending /)
- Changed default theme from dark to light
- Updated OAuth test credentials (gitea_oauth_client_id/secret)
Auth:
- First real user (excluding default admin with no password) becomes admin
- Changed user count query to exclude users without password_hash
File viewer:
- Removed separate _render_file_viewer() —
Backend:
- DELETE /api/local-workspace/items/{id} now soft-deletes (sets deleted_at
instead of hard DELETE) — items go to trash, not permanently gone
- New POST /api/local-workspace/items/{id}/restore to undo a delete
- _load_workspace_pages() and _load_children() now filter deleted_at IS NULL
so soft-deleted items disappear from sidebar and tree queries
Frontend:
- undoDelete() now calls the restore API per item (single or bulk)
- bulkDelete() now shows the same undo banner as context menu delete
- All delete paths (ctxMenu, bulk, modal) use the same soft-delete + undo flow
The context menu component (_ctxMenuData) was calling methods on
window._wsInitData (the raw JS object), NOT on Alpine's reactive proxy.
This meant that changes made by ctxMenuDelete/ctxMenuDuplicate (like
updating displayTree) were applied to the raw object but not reflected
in Alpine's reactive proxy that controls DOM rendering.
Fix: _ctxMenuData now uses a _ws() helper that returns window._wsData
(Alpine proxy, set in init()) with fallback to window._wsInitData for
safety. All context menu actions now go through the reactive proxy.
Replaced fragile _removeFromTree() + displayTree spread (which had Alpine
reactivity issues) with a clean _reloadAfterAction() call that reloads the
tree from the API. This is the same approach used by doDelete() and
doRename(), which work reliably. _reloadAfterAction also triggers the
sidebar refresh via window.appState.refreshSidebarTree().
Replaced the fragile event-based sidebar refresh with direct function calls:
- appState.init() now sets window.appState = this
- _reloadAfterAction(), doCreate(), deleteWithUndo() call
window.appState.refreshSidebarTree() directly (no DOM event)
- Sidebar handlers (drop, deleteWorkspacePage, wsCtxAction, newFolder)
call window._wsData._reloadAfterAction() directly (no DOM event)
- Removed event listeners for flowdeck:workspace-changed and
flowdeck:sidebar-refresh from local_workspace.html
This is more reliable than CustomEvent which had timing/scope issues.
deleteWithUndo() (called by ctxMenuDelete) was removing the node from the
local tree but never notifying the sidebar to refresh. Added dispatch of
'flowdeck:sidebar-refresh' event so the sidebar tree updates dynamically
without requiring a full page reload.
- Nouvel événement 'flowdeck:workspace-changed' dispatché depuis les actions
sidebar (deleteWorkspacePage, wsCtxAction rename/delete, newFolderInWorkspace)
- La vue principale local_workspace.html écoute 'flowdeck:workspace-changed'
et appelle _reloadAfterAction() pour rafraîchir son arbre
- _reloadAfterAction() continue à dispatcher 'flowdeck:sidebar-refresh'
pour le rafraîchissement sidebar uniquement (évite la boucle infinie)
- Correction du double confirm dans deleteWorkspacePage (skipConfirm=true
depuis wsCtxAction qui a déjà confirmé)
- New endpoint GET /api/sidebar/workspace-tree returns sidebar tree HTML fragment
- refreshSidebarTree() in appState() fetches and replaces #sidebar-workspace-items
- Custom event 'flowdeck:sidebar-refresh' dispatched after doCreate/doRename/doDelete
- newFolderInWorkspace, deleteWorkspacePage, wsCtxAction('rename') now use
refreshSidebarTree() instead of window.location.reload()
- Sidebar stays in sync without full page refresh
Erreur: block 'page_title' defined twice → TemplateAssertionError
Fix: title_prefix (nouveau bloc) pour <title>, page_title (existant) pour Alpine
10 templates mis à jour avec title_prefix.
Si Alpine échoue à s'initialiser, x-show n'était pas traité
et les modals apparaissaient par défaut. display:none en fallback
garantit qu'ils sont cachés, Alpine les montre via x-show.
- @click.away n'existe pas dans Alpine 3.x (renommé @click.outside)
6 occurrences corrigées dans local_workspace.html
- Ajout Alpine.data('_wsInitData') pour un binding fiable des @click
(le IIFE seul ne garantissait pas une reconnaissance Alpine propre)
1. ctxAddNewTag lit window._ctxMenuData.ctxNewTagColor (pas this.ctxNewTagColor)
→ la couleur sélectionnée dans le menu est maintenant sauvegardée
2. onContextMenu: charge workspaceTags si vide avant de calculer ctxAvailTags
→ les tags existants apparaissent dans le dropdown
3. Settings: CSS modal-overlay/modal-box/.btn-danger ajouté
→ le modal delete n'est plus transparent
4. Rename tag: modal avec input au lieu de prompt()
→ double-clic → modal avec couleur + input + autofocus
5. doFilter: _filterTreeByTag filtre displayTree quand tagFilter est actif
→ le filtrage par tags fonctionne dans toutes les vues
Context menu (_ctxMenuData):
- Remplacé getters par propriétés simples (Alpine ne réagit pas aux getters)
- Sync _wsInitData → _ctxMenuData dans onContextMenu, ctxAddNewTag, ctxRemoveTag
- Les 4 tags existants apparaissent maintenant dans le dropdown
Settings > Tags:
- Double-clic sur un tag → prompt rename (PUT /api/settings/tags/<id>)
- Suppression: modal de confirmation au lieu de confirm() natif
- Affiche le nombre d'items impactés si tag utilisé
_wsInitData (162 keys) causait "Cannot redefine property: $nextTick"
quand réutilisé comme x-data sur .ctx-menu. Alpine ajoute des propriétés
magiques ($nextTick, $el, etc.) et certaines entrent en conflit avec
les propriétés de l'objet massif.
Solution: window._ctxMenuData — objet minimal avec getters/setters
déléguant à _wsInitData. Seulement 20 propriétés, zéro conflit.
+ Restauré les directives Alpine (x-show, x-for, :style) dans le menu
puisque le scope Alpine fonctionne maintenant.
Le .ctx-menu était hors du scope _wsInitData dans le DOM (sibling du wrapper).
Ajouter x-data directement sur l'élément lui donne accès à toutes les variables
Alpine internes (ctxTagExistingOpen, ctxTagAdding, ctxAvailTags, ctxTagColors, etc.)
Les toggles de dropdown utilisent maintenant du DOM natif pour éviter les
problèmes de scope.
Backend (dashboard.py):
- POST /api/local-workspace/items/<id>/tags accepte maintenant 'color'
- INSERT INTO tags (name, color) au lieu de INSERT INTO tags (name)
- Si tag existe déjà, garde sa couleur existante
Frontend (local_workspace.html):
- ctxAddNewTag envoie {name, color} dans le body de la requête
(avant: seul {name} était envoyé, la couleur était ignorée)
Alpine ne détecte pas les changements sur ctxMenu (objet imbriqué
hors du scope _wsInitData dans le DOM). Le :style et x-show restent
bloqués sur les valeurs initiales.
Solution radicale: manipulation DOM directe
- onContextMenu: menu.style.display='block' + position left/top
- ctxMenuOpen*/Rename/Duplicate/Delete: menu.style.display='none'
- @click.outside/@keydown.escape: DOM direct aussi
- ctx-menu HTML: style="display:none" initial
x-show="ctxMenu.visible" ne réagit pas aux changements d'objet imbriqué
dans Alpine. :style avec display: ctxMenu.visible ? 'block' : 'none'
est bindé directement sur le style inline, donc réactif.
_wsInitData.onContextMenu() remplit _wsInitData.ctxMenu
→ ctxMenu = _wsInitData.ctxMenu recopie dans Alpine
→ Alpine détecte le changement et affiche le menu
Alpine ne détecte pas les changements sur les propriétés imbriquées
d'un objet (ctxMenu.visible = true). Il faut réassigner l'objet entier:
this.ctxMenu = {visible:true, x:..., y:..., node:..., tags:...}
Impact: onContextMenu (show), ctxMenuOpenPage/OpenFolder/Rename/Duplicate/Delete (hide)
+ repositionnement dans nextTick
Le Alpine proxy ne contient pas les méthodes (onContextMenu, onTouchStart, etc.)
car l'objet _wsInitData est mergé avec appState() et les fonctions ne sont
pas copiées comme propriétés directes.
Fix: _wsInitData.onContextMenu.call(, )
→ = Alpine data réactive → this.ctxMenu.visible = true fonctionne
Desktop:
- @contextmenu.prevent corrigé: window._wsData → onContextMenu()
(window._wsData n'existait pas, le handler était mort)
Mobile:
- @touchstart/@touchmove/@touchend ajoutés sur ws-split
- Détection long-press: >600ms sans mouvement >10px
- Recherche du [data-ws-id] le plus proche
- Appel onContextMenu avec les coordonnées tactiles
Fermeture:
- @click.outside ajouté sur .ctx-menu
- @click.self maintenu sur ws-split pour fermer en cliquant ailleurs
Le > de fermeture était sur la même ligne que <div class="ws-split">
→ les attributs (@contextmenu, @click, tabindex, x-ref) sur les lignes
suivantes devenaient du contenu texte affiché dans la page.
Fix: > déplacé à la fin de x-ref="layout">
Les modals (create/rename/delete) étaient des siblings de ws-split dans le DOM,
pas des enfants — à cause d'un problème de nesting HTML avec les <template> Alpine.
Résultat: x-show="showCreate" ne trouvait pas 'showCreate' dans le scope parent.
Fix: wrapper <div x-data="_wsInitData"> autour de tout le {% block content %},
plus déplacement des @dragover/@dragleave/@drop/@keydown sur ce wrapper.
Le ws-split garde ses attributs restants (@contextmenu, @click, tabindex, x-ref).
- Ajouté texte 'New File' et 'New Folder' aux boutons icônes
- Ajouté CSS .btn-create avec width:auto pour accommoder le texte
(l'ancien CSS .btn-icon imposait width:34px fixe qui coupait le texte)
- Les boutons étaient des icônes seules (📄📁) sans texte,
donc difficiles à trouver pour l'utilisateur
Alpine évalue les expressions comme tree, flatTree, online, etc.
dans un scope qui n'hérite pas toujours de _wsInitData.
En les exposant comme window.X, Alpine les trouve toujours.
Supprime les 36 erreurs restantes sur /local-workspace.
- Changé x-data="window._wsInitData" → x-data="_wsInitData"
(Alpine évalue mieux sans le préfixe global)
- Supprimé <template x-for="node in flatTree"> dans une table cachée
(x-show="false", jamais visible mais Alpine l'évaluait quand même)
Cause: l'IIFE window._wsInitData était située APRÈS le <div ws-split>.
Alpine évaluait x-data="window._wsInitData" AVANT que l'IIFE ne tourne
→ toutes les propriétés (flatTree, tree, online, ctxMenu, preview*, etc.)
étaient undefined → 148 erreurs Alpine Expression Error.
Fix: déplacement de l'IIFE (lignes 871-2405) avant le ws-split (ligne 339).
Ordre corrigé: IIFE → _wsInitData prêt → ws-split x-data OK.
Vérification: le diagnostic log « ws-split about to render, _wsInitData exists: »
affichera maintenant true au lieu de false.
Alpine 3.14.9 ne supporte pas les getters dans le Proxy x-data.
Converti en propriétés mises à jour via _recompute() dans:
- init() après chargement tree
- _loadFolder() après chaque navigation
- doSort() / doFilter() après modifs
Getters remplacés:
- pathValue, canGoBack, canGoForward, flatTree,
filteredTableItems, contentSnippetMap
- _computeFilteredTableItems() extrait de get filteredTableItems
Zéro getter restant, braces/parens équilibrés, 73 tests OK
Cause racine: local_workspace.html override le block topbar avec son
propre hamburger button qui avait encore ; au lieu de , dans l'expression
ternaire. Alpine parse les expressions via return <expr> → le ; terminait
le return prématurément → SyntaxError → cascade d'échecs d'initialisation
des composants enfants.
Corrigé dans:
- base.html: sidebar overlay @click (1 occurrence)
- local_workspace.html topbar hamburger (1 occurrence)
- local_workspace.html: 10 autres expressions (viewMode, searchQuery,
filterType, draggedItemId, tagFilter, ctxAddNewTag, navigateToPath)
73 tests OK
- x-data="_wsInitData" référence la variable globale JS (pas window.)
- var _wsInitData = IIFE — propriété window automatique pour compatibilité window._wsData
- Headers Cache-Control: no-cache, no-store, must-revalidate sur la page HTML
- 73 tests OK
- Changé function wsInit() → Alpine.data('wsInit', ...) avec addEventListener('alpine:init')
- x-data="wsInit()" → x-data="wsInit" (syntaxe correcte pour Alpine.data)
- Ajouté ?v=2.4.6 sur CSS et JS pour bust le cache navigateur
- Version bumpée à 2.4.6
- wsInit() était enregistré via Alpine.data() dans alpine:init →
ne fonctionnait pas → toutes les variables undefined (ReferenceError)
- Changé en function wsInit() globale → x-data='wsInit()' appelle
la fonction directement, zéro enregistrement Alpine
- Supprimé les wrappers Alpine.data() et addEventListener('alpine:init')
- Braces vérifiés: 397/397 équilibrés
- window._wsData.set dans init() avant await (contexte menu)
- @contextmenu.prevent passe par window._wsData (global) au lieu du scope Alpine
→ contourne tout problème de résolution de scope Alpine
- window._wsData = this déplacé AVANT le await fetch(...) dans init()
→ disponible immédiatement, pas après la réponse API
- Conserve @click.self pour fermer le menu en cliquant sur le fond
- Erreur critique: @contextmenu.prevent="onContextMenu" manquait
→ le handler n'était JAMAIS appelé par Alpine
- Rajouté @contextmenu.prevent sur ws-split (entre @keydown et @click.self)
- ctxMenu utilise mutations individuelles (pas de remplacement d'objet)
pour une meilleure réactivité Alpine
- @click.self conserve la fermeture en cliquant sur le fond du workspace
- @click.away sur ctx-menu retiré (fermait le menu immédiatement)
- Fermeture via @click.self sur ws-split (clic sur fond du workspace)
- Fermeture via Escape (déjà présent)
- Fermeture via les items du menu (ctxMenuOpenPage, etc.)
- Mode debug: console.log conservé pour confirmer l'appel handler
- Ajouté console.log('onContextMenu fired') pour diagnostiquer si le handler
Alpine est bien appelé au clic droit
- Fichier local_workspace.html restauré depuis b32b94e (sans le handler
natif cassé qui causait des erreurs Alpine)
- Vérifier la console navigateur: si 'onContextMenu fired' apparaît au
clic droit, le problème est post-handler. Sinon, c'est Alpine/l'événement
- Le handler Alpine @contextmenu.prevent="onContextMenu" ne fonctionnait pas
(problème de scope/resolution Alpine sur le clic droit)
- Remplacement par addEventListener('contextmenu', ...) natif dans init()
→ événement capturé directement sur le DOM, bypass complet Alpine
- Propriétés ctxMenu modifiées individuellement (pas d'objet remplacé)
pour garantir la réactivité Alpine sur les nested properties
- L'ancien handler onContextMenu est gardé en fallback avec le même pattern
- Suppression du @contextmenu.prevent du template HTML (évite double-fire)
- @click.outside détectait le clic-droit d'ouverture comme 'outside' → menu
fermé immédiatement après ouverture. Même bug que le dropdown sidebar.
- @click.away utilise setTimeout → le handler s'enregistre APRÈS le cycle
d'événement courant → le clic d'ouverture n'est pas capté comme 'away'.
- Fonctionne dans TOUTES les vues: tree (renderChildren), list, details,
cards, content — elles ont toutes data-ws-id