- 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é)
- Extracted render_workspace_tree macro into _workspace_tree_macro.html
so it can be imported independently from base.html (which has many
template variables like user, workspace_name, etc.)
- Fixed GET /api/sidebar/workspace-tree to use the extracted macro
- Added error logging for easier debugging
- 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
- loadGiteaTree: skip if giteaWorkspace component already loaded tree
- loadGiteaTree: check r.ok, add console.error logging, show error in UI
- loadGiteaTree: use .bind(this) for correct this in callbacks
- gitea_workspace.html: replace querySelector('[x-data]') with captured self
in loadSidebarTree, loadSubdir, Private Pages click handlers
- Prevents loadGiteaTree from overwriting loadSidebarTree results
on /gitea-workspace page
- Opening /gitea-workspace auto-creates a local workspace with same name
- New Page/New Folder in remote context → saves to local mirror workspace
- Sidebar stays focused on remote workspace (🔗 icon, remote tree)
- local_ws_id passed to Alpine for API calls
- /api/local-workspace/items accepts workspace_id in body
Bug 1: newPageInWorkspace() allait toujours vers local-workspace API
→ Maintenant détecte workspaceKey contient '/' → Gitea API PUT file
Bug 2: workspace_name dans gitea_workspace ne persistait pas
→ Cookie flowdeck_workspace posé côté serveur au premier chargement
Bug 3: icône 📁 fixe dans sidebar — pas d'indication remote
→ 🔗 affiché quand workspace_key contient '/' (remote), 📁 sinon
newFolderInWorkspace() : avertissement si workspace distant
(car les dossiers n'existent pas dans un repo Git)
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.
Téléchargés et servis depuis /static/js/:
- alpine.min.js (44 KB) — remplace unpkg.com
- htmx.min.js (51 KB) — remplace unpkg.com
- sortable.min.js (45 KB) — remplace cdn.jsdelivr.net
- prism.min.js (19 KB) + prism.css (1.3 KB) — remplace cdnjs
Corrige le bug 'rien ne fonctionne' quand le réseau est lent/absent:
sans CDN, Alpine.js/HTMX/SortableJS ne chargeaient pas →
tous les @click, x-show, x-data, drag-drop inopérants.
Alpine 3.x a renommé .away en .outside. Toutes les occurrences dans:
- base.html (3: user menu, context menu, ws ctx)
- page_editor.html (5: share, perm, access, more, gsMore)
- workspaces.html (1: dialog overlay)
- workspace.html (1: modal overlay)
- local_workspace.html (6: create, rename, delete modals, context menus)
Ces .away cassées empêchaient tous les @click Alpine de fonctionner
sur ces pages (Cancel, Delete, et tous les autres boutons modaux).
- POST /api/gitea/projects/{owner}/{repo}/upload (binary upload)
- get_file_commits() in GiteaClient
- GET /api/gitea/projects/{owner}/{repo}/commits (per-file history)
- POST .../sync-labels (Gitea labels → FlowDeck tags)
- New file form + upload button in gitea_workspace.html topbar
Problème: window.history.back() restaurait la page depuis bfcache
→ le IIFE initTheme() ne se ré-exécutait pas
→ le thème dark s'affichait même après avoir sélectionné light
Fix:
- applySavedTheme() → fonction nommée, pas IIFE
- window.addEventListener('pageshow', e.persisted → reapply)
- Proper else branch: removeProperty pour le dark mode
- Sidebar: retrait du bouton 🌓 (dark/light) + bouton 🚪 logout
- Settings → Account → Preferences: nouveau sélecteur de thème (Dark/Light)
- Persistance localStorage 'fd_theme' — appliqué au chargement de chaque page
- initTheme() dans base.html + applyTheme() dans settings.html
- toggleTheme() conservé dans le JS mais plus utilisé dans l'UI
Avant: _sidebar_data() utilisait asyncio.run() → RuntimeWarning + 302 redirects
Après: gitea_workspace.html appelle loadSidebarTree() → fetch API → injecte dans #sidebar-workspace-items
- base.html: ajout id='sidebar-workspace-items' sur le <ul> workspace
- dashboard.py: _sidebar_data() ne fait plus de fetch serveur, juste passe owner/repo
1. Tab user repos: fetch username via /auth/user → affiche 'bruno' au lieu de 'Personal'
2. Sidebar: ws_cookie='gitea:owner:repo' → _sidebar_data() fetch le tree depuis Gitea API
3. base.html: data-gitea-path/type/sha sur les items workspace pour les projets Gitea
4. gitea_workspace.html: refonte sans tree panel → navigation via sidebar
- Clic fichier → ouvre dans le contenu (lecture + edit + commit)
- Clic dossier → lazy-load les enfants dans le sidebar
5. openGitea(): set cookie flowdeck_workspace avant navigation
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
Le hamburger button avait mobileSidebarOpen = true; sidebarCollapsed = false
Le ; casse le parser d'expression Alpine (return <expr> → le ; termine le statement)
En cascade, l'erreur empêche l'initialisation correcte du composant parent,
ce qui brise tous les composants enfants (wsInit → ctxMenu, modals, preview, etc.)
C'est le bug qui persistait depuis des jours à travers 5+ approches différentes.
- 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
- Bouton show sidebar (uncollapse) supprimé de position:fixed → intégré au
hamburger du topbar. Si sidebar collapsed → hamburger l'ouvre directement.
Plus aucun chevauchement avec le contenu.
- .ws-split padding: 40px 24px 0 → 8px 16px 0 (gain 32px verticaux)
- Tous les éléments sont remontés, plus d'espace visible sans scroll
- Route manquante GET /api/settings/avatar/{filename} → sert le fichier uploadé
(résout le 404 sur l'image uploadée)
- Colonne avatar_color ajoutée à users (migration ALTER TABLE try/except)
- set_avatar_color sauvegarde la couleur dans la DB (pas seulement local)
- _sidebar_data() lit avatar_url + avatar_color depuis la DB
- Sidebar avatar: fond coloré quand pas d'image, image quand uploadée
avec background-image:url() + initiale en fallback
1. @click="workspaceMenuOpen = !workspaceMenuOpen" inline (pas toggleWorkspaceMenu)
2. @click.away (pas click.outside) — click.away ignore le clic sur le trigger
3. Wrapper <div style=position:relative> autour header+dropdown →
position:absolute s'ancre correctement, header et dropdown sont siblings
donc le clic header ne déclenche PAS le @click.away du dropdown
- folderStack: supprimé {id:0, name:'Root'} dans init() et navigateToRoot()
→ breadcrumb n'affiche plus 'Workspace / Root' mais juste 'Workspace / dossier'
- Sidebar: overflow-y:auto déplacé de .sidebar vers .sidebar-scroll (flex child)
→ le header et son dropdown ne sont PLUS clippés par overflow
→ position: absolute + top:100% fonctionne maintenant sur le dropdown
- .sidebar-scroll wrapper autour du contenu scrollable (nav row → footer)
- Retiré le bouton collapse en double dans la nav row (déjà dans le header)
- Ajouté position: relative sur .sidebar-workspace-header → dropdown s'ancre
correctement sous le header au lieu de se positionner n'importe où
- Sans position: relative, le dropdown absolute remontait jusqu'à un ancêtre
positionné et sortait de l'écran ou était caché par overflow
- Collapse << dans le header (opacity 0, visible au hover avec @click.stop)
- Search 🔍 déplacé à droite du collapse dans la nav row
- Nom affiché: user.full_name ou user.login (pas workspaceName)
- Chevron ▾ conservé comme indicateur dropdown (tout le header reste cliquable)
- Le patch précédent a retiré le wrapper <div style="display:flex..."> mais
son </div> fermant est resté → balise fermante orpheline → DOM cassé
- Résultat: la page ne s'affiche plus du tout (rendu HTML invalide)
- Sidebar header: user menu dropdown (Settings, Switch workspace, Log out)
avec avatar + nom + email, chevron animé, fond opaque #1E1E1E
- Nouvelle page /settings avec gestion globale des tags:
créer tag avec couleur, changer couleur, supprimer, liste avec compteurs
- API tag management: POST/PUT/DELETE /api/settings/tags, GET /api/settings/tags/all
- Preview panel: affichage images (img tag), PDF (lien viewer), détection format
- workspace-chevron CSS: rotation 180° quand menu ouvert
Implémenté depuis la ROADMAP v2.2.0:
A3 Inline rename: double-clic → input inline (Enter/escape/blur).
Fonctionne sur x-for items et breadcrumb.
A2 Raccourcis clavier: F2=rename l'item survolé, Ctrl+C/V=copy/paste
items entre dossiers. Layout focusable (tabindex=0, @keydown).
A6 Duplicate: méthode duplicateItem(), crée une copie '(copy)'.
B3 Métadonnées: child_count ajouté dans l'API tree + sidebar data.
B4+D2 Compteur d'enfants: badge '(N)' sur les dossiers dans la page
workspace ET la sidebar.
B5 Loading skeletons: CSS shimmer animation (.skeleton).
C1 Breadcrumb éditable: double-clic sur un segment → rename.
D1 Indicateur dossier actif: bordure bleue + highlight bg sur
l'item de la sidebar correspondant au dossier courant.
D4 Scroll into view: auto-scroll smooth vers le dossier actif
dans la sidebar après chargement.
child_count ajouté à _build_tree_children (dashboard.py) et
_load_workspace_pages/_load_children (board.py).
Sidebar workspace:
- Arbre plus compact: padding réduit (4+12*depth au lieu de 8+16),
gap 4px, min-height 24px, font 13px, chevron 12px
- Boutons d'action (📄📁✏️🗑) remplacés par menu contextuel
au clic droit (@contextmenu.prevent) et long-press mobile (500ms)
- Menu: New File, New Folder (si dossier), Rename, Delete
- Nom de l'item affiché en bas du menu
Page workspace:
- Boutons ⊞ (Expand All) et ⊟ (Collapse All) dans la toolbar
- expandAll(): parcourt l'arbre, set expanded[id]=true, toggle DOM
- collapseAll(): vide expanded, masque tous les <ul>
Root cause: sectionsOpen did not include workspace:true by default,
and toggle state was not persisted. On every page navigation,
sectionsOpen.workspace defaulted to undefined (falsy), hiding the
workspace tree section.
Fixes:
- sectionsOpen now defaults with workspace:true
- sectionsOpen persisted to localStorage on toggle and before navigation
- beforeunload handler auto-saves expandedFolders + sectionsOpen
(covers <a href> navigation for file clicks too)
- navigateToFolder saves both expandedFolders and sectionsOpen
1. Drag & drop interne: déplacer fichiers/dossiers existants vers d'autres
dossiers ou racine via l'API /api/local-workspace/items/{id}/move.
Items rendus draggable dans le template x-for et renderChildren.
2. File viewer pour fichiers uploadés: détection content_format='file',
visualiseur intégré avec rendu adapté au type MIME:
- Images: affichage direct
- PDF: iframe
- Texte/Code (.py .js .md .html .ps1 etc.): <pre> avec fetch du contenu
- Autres: lien de téléchargement
Route GET /api/files/{ws_id}/{filename:path} pour servir les fichiers.
3. Sidebar persistante: expandedFolders sauvegardé dans localStorage,
restauré au chargement, préservé avant navigation vers un dossier.
4. Suppression: nettoie aussi le fichier disque pour content_format='file'
- API /api/local-workspace/tree: arbre récursif complet (support optionnel ?folder=ID)
- API /api/local-workspace/breadcrumb: fil d'Ariane parent pour un dossier
- Page /local-workspace: supporte ?folder=ID pour naviguer dans un dossier
- Sidebar: expand/collapse (▶/▼) sur les dossiers, clic dossier → /local-workspace?folder=ID
- CSS: styles chevron .tree-chevron, .tree-folder-link
- Alpine.js: expandedFolders, toggleTreeFolder, navigateToFolder
Both sidebar and workspace page DnD handlers now use @drop.prevent
to stop browser's default text insertion behavior. Without .prevent,
the browser tries to navigate to the dragged text as a URL, breaking
the drop event.
Added x-cloak directive and [x-cloak] CSS so button never flashes
before Alpine loads. Button is in base.html so appears on all pages
that extend the base template.
When sidebarCollapsed=true, a hamburger button appears at top-left
corner to restore the sidebar. CSS fixed positioning outside sidebar
so it's always visible even when sidebar is collapsed.
- Topbar: removed New File/New Folder buttons + 'Files' label
- Breadcrumb: Workspace / work1 instead of work1 / Files
- Modals: single shared create modal with focus auto, @keydown.enter
- parent_id param: recursive folder/file creation inside folders
- Sidebar buttons use same POST /api/local-workspace/items
- newPageInWorkspace/newFolderInWorkspace now both use unified API
- Login redirect (all modes) now goes to /workspaces
- Workspace tree API filters by active workspace_id (no leak)
- Workspace name in sidebar from cookie (active_ws_name)
- Sidebar workspace section: 📄 new page + 📁 new folder buttons
- deleteWorkspacePage() in sidebar with confirmation
- _load_workspace_pages() helper with workspace_id filter
- CSS for sidebar-item-delete button (× on hover)
- Home button now links to /workspaces (universal workspace page)
- Sidebar workspace section shows active workspace name from cookie
- _sidebar_data reads flowdeck_workspace cookie for active_ws_name
- local-workspace APIs filter by workspace_id (not global)
- newPageInWorkspace() JS function added to sidebar
- CSRF exemption for /api/local-workspace routes