fix: vues workspace affichent répertoire courant + navigation sans rechargement
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Vues list/details/title/content utilisent displayTree (enfants du dossier courant)
  au lieu de flatTree (arbre entier aplati) → comportement explorateur de fichiers
- Navigation sans rechargement: navigateToFolder/goToParent/goBack/goForward
  utilisent fetch AJAX + _loadFolder() au lieu de window.location
- folderStack remplace navHistory + breadcrumb: pile de navigation avec noms
- Breadcrumb dynamique: cliquable, affiche le chemin courant depuis folderStack
- _reloadAfterAction utilise _loadFolder au lieu de window.location.reload
- La vue choisie (tree/list/details/title/content) est préservée pendant la navigation
This commit is contained in:
2026-07-12 11:37:10 -04:00
parent e36dead312
commit 21806aa14e
+103 -47
View File
@@ -311,20 +311,20 @@
</div>
<!-- Breadcrumb -->
<div class="breadcrumb-bar" x-show="breadcrumb.length > 0">
<a href="/local-workspace">📁 {{ workspace_name }}</a>
<template x-for="(b, i) in breadcrumb" :key="b.id">
<div class="breadcrumb-bar" x-show="folderStack.length > 0">
<a href="/local-workspace" @click.prevent="navigateToRoot()">📁 {{ workspace_name }}</a>
<template x-for="(b, i) in folderStack" :key="'bc'+b.id">
<span style="display:contents;">
<span class="sep">/</span>
<a :href="'/local-workspace?folder=' + b.id" x-text="b.name"
:class="{ current: i === breadcrumb.length - 1 }"
<a href="#" @click.prevent="navigateToBreadcrumb(i)" x-text="b.name"
:class="{ current: i === folderStack.length - 1 }"
@dblclick.prevent="startInlineRename(b, $event)"></a>
</span>
</template>
</div>
<!-- Path bar -->
<div class="path-bar" x-show="breadcrumb.length > 0">
<div class="path-bar" x-show="folderStack.length > 0">
<span class="path-icon">📂</span>
<input class="path-input" :value="pathValue"
@keydown.enter="navigateToPath()"
@@ -334,9 +334,9 @@
</div>
<div class="ws-header">
<h1 x-show="breadcrumb.length===0">📁 {{ workspace_name }}</h1>
<h1 x-show="breadcrumb.length>0">
📁 <span x-text="breadcrumb.length > 0 ? breadcrumb[breadcrumb.length-1].name : ''"></span>
<h1 x-show="folderStack.length===0 || (folderStack.length===1 && folderStack[0].id===0)">📁 {{ workspace_name }}</h1>
<h1 x-show="folderStack.length>0 && !(folderStack.length===1 && folderStack[0].id===0)">
📁 <span x-text="folderStack[folderStack.length-1].name"></span>
</h1>
<div class="ws-toolbar">
<button class="btn-icon" @click="goBack()" title="Back" :disabled="!canGoBack">←</button>
@@ -392,7 +392,7 @@
<ul class="ws-tree" x-show="viewMode==='tree' && (displayTree.length>0 || (currentFolder > 0))" @click="handleTreeAction($event)">
<!-- Parent folder ".." -->
<li x-show="currentFolder > 0">
<li x-show="currentFolder > 0" style="list-style:none;">
<div class="ws-tree-item" @click="goToParent()" style="padding-left:4px;">
<span style="width:16px;flex-shrink:0;"></span>
<span class="icon">📂</span>
@@ -534,7 +534,7 @@
</table>
<!-- List view — compact flat list -->
<table class="ws-table-compact" x-show="viewMode==='list' && !searchQuery && !filterType && !tagFilter && flatTree.length>0">
<table class="ws-table-compact" x-show="viewMode==='list' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
<thead><tr>
<th style="width:24px"></th>
<th>Name</th>
@@ -543,7 +543,7 @@
<th style="width:100px">Modified</th>
</tr></thead>
<tbody>
<template x-for="node in flatTree" :key="'l'+node.id">
<template x-for="node in displayTree" :key="'l'+node.id">
<tr @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" style="cursor:pointer">
<td><span x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span></td>
<td class="col-name" x-text="node.name"></td>
@@ -556,7 +556,7 @@
</table>
<!-- Details view — full table with all columns -->
<table class="ws-table-enhanced" x-show="viewMode==='details' && !searchQuery && !filterType && !tagFilter && flatTree.length>0">
<table class="ws-table-enhanced" x-show="viewMode==='details' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
<thead><tr>
<th style="width:24px"></th>
<th>Name</th>
@@ -568,7 +568,7 @@
<th>Tags</th>
</tr></thead>
<tbody>
<template x-for="node in flatTree" :key="'d'+node.id">
<template x-for="node in displayTree" :key="'d'+node.id">
<tr @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" style="cursor:pointer"
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
<td><span class="table-icon" x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span></td>
@@ -591,8 +591,8 @@
</table>
<!-- Title view — card grid -->
<div class="title-grid" x-show="viewMode==='title' && !searchQuery && !filterType && !tagFilter && flatTree.length>0">
<template x-for="node in flatTree" :key="'g'+node.id">
<div class="title-grid" x-show="viewMode==='title' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
<template x-for="node in displayTree" :key="'g'+node.id">
<div class="title-card" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)"
:class="{ 'selected-card': previewItem && previewItem.id === node.id }">
<div class="title-card-icon" x-text="node.is_folder ? '📁' : _fileIcon(node.name, node.is_folder, node.content_format)"></div>
@@ -606,8 +606,8 @@
</div>
<!-- Content view — list with inline preview -->
<div class="content-list" x-show="viewMode==='content' && !searchQuery && !filterType && !tagFilter && flatTree.length>0">
<template x-for="node in flatTree" :key="'c'+node.id">
<div class="content-list" x-show="viewMode==='content' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
<template x-for="node in displayTree" :key="'c'+node.id">
<div class="content-item" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)"
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
<div class="content-item-header">
@@ -654,7 +654,7 @@
<div class="modal-box">
<h3><span x-text="createType==='folder'?'📁':'📄'"></span> <span x-text="createType==='folder'?'New Folder':'New File'"></span></h3>
<p class="modal-sub">
In <strong>{{ workspace_name }}</strong>
In <strong>{{ workspace_name }}</strong> <span x-show="folderStack.length>1">/ <span x-text="folderStack[folderStack.length-1].name"></span></span>
<span x-show="parentFolder"> / <span x-text="parentFolder?.name"></span></span>
</p>
<input class="modal-input" x-model="newName" :placeholder="createType==='folder'?'Folder name':'File name'" @keydown.enter="doCreate" x-ref="cinp">
@@ -748,6 +748,7 @@ document.addEventListener('alpine:init', function() {
currentFolder: {{ current_folder_id }},
searchQuery: '',
filterType: '',
folderStack: [],
// Inline rename
renamingId: null,
@@ -790,7 +791,22 @@ document.addEventListener('alpine:init', function() {
// Path bar
get pathValue() {
return '/workspace/' + this.breadcrumb.map(function(b){return b.name}).join('/');
return '/workspace/' + this.folderStack.filter(function(b){return b.id > 0}).map(function(b){return b.name}).join('/');
},
async navigateToRoot() {
this.folderStack = [{id: 0, name: 'Root'}];
this.navIndex = 0;
this._saveExpandedState();
await this._loadFolder(0);
},
async navigateToBreadcrumb(idx) {
this.folderStack = this.folderStack.slice(0, idx + 1);
this.navIndex = idx;
this._saveExpandedState();
var entry = this.folderStack[idx];
await this._loadFolder(entry ? entry.id : 0);
},
// Drag indicators
@@ -808,7 +824,7 @@ document.addEventListener('alpine:init', function() {
navHistory: [],
navIndex: -1,
get canGoBack() { return this.navIndex > 0; },
get canGoForward() { return this.navIndex < this.navHistory.length - 1; },
get canGoForward() { return this.navIndex < this.folderStack.length - 1; },
// Preview popup
previewVisible: false,
@@ -841,9 +857,17 @@ document.addEventListener('alpine:init', function() {
this.displayTree = roots;
this.breadcrumb = d.breadcrumb || [];
this.loaded = true;
// Initialize nav history
this.navHistory = [this.currentFolder || 0];
this.navIndex = 0;
// Initialize folder stack from breadcrumb
this.folderStack = (this.breadcrumb || []).map(function(b) { return {id: b.id, name: b.name}; });
if (this.currentFolder && this.currentFolder > 0 && this.folderStack.length === 0) {
this.folderStack = [{id: 0, name: 'Root'}];
// Add current folder name from tree if present
if (this.tree.length > 0) {
// The current folder's children are in breadcrumb, parent is the last
}
}
this.navIndex = this.folderStack.length > 0 ? this.folderStack.length - 1 : 0;
if (this.folderStack.length === 0) this.folderStack = [{id: 0, name: 'Root'}];
// Expose globally for renderChildren onclick handlers
window._wsData = this;
// Listen for online/offline
@@ -1159,37 +1183,69 @@ document.addEventListener('alpine:init', function() {
}
},
navigateToFolder(id) {
// Push to nav history
if (this.navIndex < this.navHistory.length - 1) {
this.navHistory = this.navHistory.slice(0, this.navIndex + 1);
async navigateToFolder(id) {
// Find the folder name for the stack
var folderNode = this._findNodeById(this.tree, id);
var folderName = folderNode ? folderNode.name : 'Folder';
// Push to folder stack for breadcrumb
if (this.navIndex < this.folderStack.length - 1) {
this.folderStack = this.folderStack.slice(0, this.navIndex + 1);
}
this.navHistory.push(id);
this.navIndex = this.navHistory.length - 1;
this._saveExpandedState();
window.location = '/local-workspace?folder=' + id;
this.folderStack.push({id: id, name: folderName});
this.navIndex = this.folderStack.length - 1;
// Fetch children of this folder
await this._loadFolder(id);
},
goBack() {
async goToParent() {
if (this.folderStack.length <= 1) { await this._loadFolder(0); return; }
this.folderStack.pop();
this.navIndex = this.folderStack.length - 1;
var parentId = this.folderStack.length > 0 ? this.folderStack[this.folderStack.length - 1].id : 0;
await this._loadFolder(parentId);
},
async _loadFolder(folderId) {
var url = '/api/local-workspace/tree';
if (folderId && folderId > 0) url += '?folder=' + folderId;
try {
var r = await fetch(url);
var d = await r.json();
var roots = d.tree || [];
this._markTree(roots, 0);
this.tree = roots;
this.displayTree = [...roots];
this.breadcrumb = d.breadcrumb || [];
this.currentFolder = folderId || 0;
this._saveExpandedState();
this.previewItem = null;
} catch(e) { console.error('Load folder error:', e); }
},
_findNodeById(nodes, id) {
for (var i=0; i<nodes.length; i++) {
if (nodes[i].id === id) return nodes[i];
if (nodes[i].children) {
var found = this._findNodeById(nodes[i].children, id);
if (found) return found;
}
}
return null;
},
async goBack() {
if (this.navIndex <= 0) return;
this.navIndex--;
var id = this.navHistory[this.navIndex];
var entry = this.folderStack[this.navIndex];
this._saveExpandedState();
window.location = id ? '/local-workspace?folder=' + id : '/local-workspace';
await this._loadFolder(entry ? entry.id : 0);
},
goForward() {
if (this.navIndex >= this.navHistory.length - 1) return;
async goForward() {
if (this.navIndex >= this.folderStack.length - 1) return;
this.navIndex++;
var id = this.navHistory[this.navIndex];
var entry = this.folderStack[this.navIndex];
this._saveExpandedState();
window.location = id ? '/local-workspace?folder=' + id : '/local-workspace';
},
goToParent() {
var bc = this.breadcrumb;
if (bc.length <= 1) { window.location = '/local-workspace'; return; }
var parentId = bc[bc.length - 2].id;
this._saveExpandedState();
window.location = '/local-workspace?folder=' + parentId;
await this._loadFolder(entry ? entry.id : 0);
},
_saveExpandedState() {
@@ -1198,7 +1254,7 @@ document.addEventListener('alpine:init', function() {
},
_reloadAfterAction() {
this._saveExpandedState();
window.location.reload();
this._loadFolder(this.currentFolder);
},
// ── Inline Rename ──