fix: 5 bugs — couleur tag, tags existants, modal settings, rename, filtrage
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

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
This commit is contained in:
2026-07-13 11:34:43 -04:00
parent 53865f136d
commit 51c6002f08
2 changed files with 69 additions and 6 deletions
+25 -2
View File
@@ -751,6 +751,10 @@ window._wsInitData = (function() {
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;
@@ -868,7 +872,7 @@ window._wsInitData = (function() {
if (!tagName) return;
var node = this.ctxMenu.node;
if (!node) return;
color = color || this.ctxNewTagColor;
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})
@@ -1393,9 +1397,28 @@ window._wsInitData = (function() {
var q = (this.searchQuery || '').toLowerCase().trim();
var ft = this.filterType || '';
if (!q && !ft && !this.tagFilter) { this.displayTree = [...this.tree]; this._computeFilteredTableItems(); return; }
this.displayTree = [...this.tree];
if (this.tagFilter) {
// Filter tree by tag
this.displayTree = this._filterTreeByTag([...this.tree], this.tagFilter);
} else {
this.displayTree = [...this.tree];
}
this._computeFilteredTableItems();
},
_filterTreeByTag(nodes, tagName) {
var result = [];
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
var hasTag = (n.tags || []).some(function(t) { return t.name === tagName; });
var filteredChildren = n.children ? this._filterTreeByTag(n.children, tagName) : [];
if (hasTag || filteredChildren.length > 0) {
var clone = Object.assign({}, n);
clone.children = filteredChildren;
result.push(clone);
}
}
return result;
},
filterByType(type) {
this.filterType = (this.filterType === type) ? '' : type;
this.doFilter();
+44 -4
View File
@@ -57,6 +57,13 @@
.color-swatch:hover{transform:scale(1.15);}
.color-swatch.selected{border-color:var(--text);box-shadow:0 0 0 2px var(--accent);}
/* Modal (for tag delete/rename) */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:999;backdrop-filter:blur(4px);}
.modal-box{background:var(--bg);border:1px solid var(--border-strong);border-radius:12px;padding:24px;min-width:320px;box-shadow:0 12px 40px rgba(0,0,0,.5);}
.modal-box h3{font-size:16px;font-weight:600;color:var(--text);}
.btn-danger{background:#E03E3E;color:#fff;border:none;padding:6px 16px;border-radius:6px;cursor:pointer;font-size:13px;}
.btn-danger:hover{background:#C93535;}
/* Close button */
.settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;background:none;border:none;color:var(--text-dim);font-size:20px;cursor:pointer;border-radius:6px;display:flex;align-items:center;justify-content:center;}
.settings-close:hover{background:var(--bg-hover);color:var(--text);}
@@ -218,7 +225,7 @@
<p style="color:var(--text-dim);margin:0 0 16px;">
Are you sure you want to delete the tag "<strong x-text="deletingTag?.name"></strong>"?
<span x-show="deletingTag?.count > 0" style="color:#E03E3E;display:block;margin-top:4px;">
⚠️ This tag is used on <span x-text="deletingTag?.count"></span> item(s). Removing it will also remove it from those items.
⚠️ This tag is used on <span x-text="deletingTag?.count"></span> item(s).
</span>
</p>
<div style="display:flex;gap:8px;justify-content:flex-end;">
@@ -227,6 +234,22 @@
</div>
</div>
</div>
<!-- Rename modal -->
<div class="modal-overlay" x-show="renamingTag" @click.self="renamingTag=null" @keydown.escape="renamingTag=null" style="z-index:1000;">
<div class="modal-box" style="max-width:360px;">
<h3 style="margin:0 0 12px;">Rename tag</h3>
<div style="display:flex;gap:8px;align-items:center;">
<div class="tag-color-dot" :style="{background: renamingTag?.color}" style="width:16px;height:16px;"></div>
<input type="text" class="settings-input" x-model="renameValue"
@keydown.enter="confirmRenameTag()" @keydown.escape="renamingTag=null"
style="flex:1;" autofocus>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;">
<button class="btn" @click="renamingTag=null">Cancel</button>
<button class="btn btn-primary" @click="confirmRenameTag()" :disabled="!renameValue.trim()">Rename</button>
</div>
</div>
</div>
</div>
<!-- Features -->
@@ -288,6 +311,8 @@ document.addEventListener('alpine:init', function() {
newTagName: '',
newTagColor: '#787774',
deletingTag: null,
renamingTag: null,
renameValue: '',
presetColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
avatarUrl: '{{ avatar_url or "" }}',
avatarColor: '{{ avatar_color or "#3A3A3A" }}',
@@ -332,12 +357,27 @@ document.addEventListener('alpine:init', function() {
},
async renameTag(tag) {
var newName = prompt('Rename tag "' + tag.name + '":', tag.name);
if (!newName || !newName.trim() || newName.trim().toLowerCase() === tag.name.toLowerCase()) return;
this.renamingTag = tag;
this.renameValue = tag.name;
// Focus the input after Alpine renders the modal
var self = this;
this.$nextTick(function() {
var input = document.querySelector('.modal-box input[type=text]');
if (input) { input.focus(); input.select(); }
});
},
async confirmRenameTag() {
var tag = this.renamingTag;
var newName = this.renameValue.trim();
if (!tag || !newName || newName.toLowerCase() === tag.name.toLowerCase()) {
this.renamingTag = null; return;
}
await fetch('/api/settings/tags/' + tag.id, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({name: newName.trim()})
body: JSON.stringify({name: newName})
});
this.renamingTag = null;
await this.loadTags();
},