mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
refactor(sidebar): inline new-folder row, drop drag-to-blank creation (#999)
- Render the new-folder input as a temporary tree row at the creation location (file-explorer style): full-width input confirmed with Enter and canceled with Escape/blur; the parent folder auto-expands, and in list mode the row is inserted after the parent item - Remove the drag-to-blank-area folder creation (drop-zone strip, sidebar-level drag handlers, performDragMoveWithState); dropping models onto folder nodes still moves them - Update empty-state hints and locale keys accordingly
This commit is contained in:
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "Zielpfad für das Verschieben konnte nicht ermittelt werden.",
|
||||
"moveUnsupported": "Verschieben wird für dieses Element nicht unterstützt.",
|
||||
"createFolderHint": "Loslassen, um einen neuen Ordner zu erstellen",
|
||||
"newFolderName": "Neuer Ordnername",
|
||||
"folderNameHint": "Eingabetaste zum Bestätigen, Escape zum Abbrechen",
|
||||
"emptyFolderName": "Bitte geben Sie einen Ordnernamen ein",
|
||||
"invalidFolderName": "Ordnername enthält ungültige Zeichen",
|
||||
"noDragState": "Kein ausstehender Ziehvorgang gefunden"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "Keine Ordner gefunden",
|
||||
"dragHint": "Elemente hierher ziehen, um Ordner zu erstellen",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Auf Updates in diesem Ordner prüfen",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "Unable to determine destination path for move.",
|
||||
"moveUnsupported": "Move is not supported for this item.",
|
||||
"createFolderHint": "Release to create new folder",
|
||||
"newFolderName": "New folder name",
|
||||
"folderNameHint": "Press Enter to confirm, Escape to cancel",
|
||||
"emptyFolderName": "Please enter a folder name",
|
||||
"invalidFolderName": "Folder name contains invalid characters",
|
||||
"noDragState": "No pending drag operation found"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "No folders found",
|
||||
"dragHint": "Drag items here to create folders",
|
||||
"createHint": "Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Check for updates in this folder",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "No se puede determinar la ruta de destino para el movimiento.",
|
||||
"moveUnsupported": "El movimiento no es compatible con este elemento.",
|
||||
"createFolderHint": "Suelta para crear una nueva carpeta",
|
||||
"newFolderName": "Nombre de la nueva carpeta",
|
||||
"folderNameHint": "Presiona Enter para confirmar, Escape para cancelar",
|
||||
"emptyFolderName": "Por favor, introduce un nombre de carpeta",
|
||||
"invalidFolderName": "El nombre de la carpeta contiene caracteres no válidos",
|
||||
"noDragState": "No se encontró ninguna operación de arrastre pendiente"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "No se encontraron carpetas",
|
||||
"dragHint": "Arrastra elementos aquí para crear carpetas",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Buscar actualizaciones en esta carpeta",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "Impossible de déterminer le chemin de destination pour le déplacement.",
|
||||
"moveUnsupported": "Le déplacement n'est pas pris en charge pour cet élément.",
|
||||
"createFolderHint": "Relâcher pour créer un nouveau dossier",
|
||||
"newFolderName": "Nom du nouveau dossier",
|
||||
"folderNameHint": "Appuyez sur Entrée pour confirmer, Échap pour annuler",
|
||||
"emptyFolderName": "Veuillez saisir un nom de dossier",
|
||||
"invalidFolderName": "Le nom du dossier contient des caractères invalides",
|
||||
"noDragState": "Aucune opération de glissement en attente trouvée"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "Aucun dossier trouvé",
|
||||
"dragHint": "Faites glisser des éléments ici pour créer des dossiers",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Vérifier les mises à jour dans ce dossier",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "לא ניתן לקבוע את נתיב היעד להעברה.",
|
||||
"moveUnsupported": "העברה אינה נתמכת עבור פריט זה.",
|
||||
"createFolderHint": "שחרר כדי ליצור תיקייה חדשה",
|
||||
"newFolderName": "שם תיקייה חדשה",
|
||||
"folderNameHint": "הקש Enter לאישור, Escape לביטול",
|
||||
"emptyFolderName": "אנא הזן שם תיקייה",
|
||||
"invalidFolderName": "שם התיקייה מכיל תווים לא חוקיים",
|
||||
"noDragState": "לא נמצאה פעולת גרירה ממתינה"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "לא נמצאו תיקיות",
|
||||
"dragHint": "גרור פריטים לכאן כדי ליצור תיקיות",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "בדוק עדכונים בתיקייה זו",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "移動先のパスを特定できません。",
|
||||
"moveUnsupported": "この項目の移動はサポートされていません。",
|
||||
"createFolderHint": "放して新しいフォルダを作成",
|
||||
"newFolderName": "新しいフォルダ名",
|
||||
"folderNameHint": "Enterで確定、Escでキャンセル",
|
||||
"emptyFolderName": "フォルダ名を入力してください",
|
||||
"invalidFolderName": "フォルダ名に無効な文字が含まれています",
|
||||
"noDragState": "保留中のドラッグ操作が見つかりません"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "フォルダが見つかりません",
|
||||
"dragHint": "ここへアイテムをドラッグしてフォルダを作成します",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "このフォルダのアップデートを確認",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "이동할 대상 경로를 확인할 수 없습니다.",
|
||||
"moveUnsupported": "이 항목은 이동을 지원하지 않습니다.",
|
||||
"createFolderHint": "놓아서 새 폴더 만들기",
|
||||
"newFolderName": "새 폴더 이름",
|
||||
"folderNameHint": "Enter를 눌러 확인, Escape를 눌러 취소",
|
||||
"emptyFolderName": "폴더 이름을 입력하세요",
|
||||
"invalidFolderName": "폴더 이름에 잘못된 문자가 포함되어 있습니다",
|
||||
"noDragState": "보류 중인 드래그 작업을 찾을 수 없습니다"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "폴더를 찾을 수 없습니다",
|
||||
"dragHint": "항목을 여기로 드래그하여 폴더를 만듭니다",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "이 폴더의 업데이트 확인",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "Не удалось определить путь назначения для перемещения.",
|
||||
"moveUnsupported": "Перемещение этого элемента не поддерживается.",
|
||||
"createFolderHint": "Отпустите, чтобы создать новую папку",
|
||||
"newFolderName": "Имя новой папки",
|
||||
"folderNameHint": "Нажмите Enter для подтверждения, Escape для отмены",
|
||||
"emptyFolderName": "Пожалуйста, введите имя папки",
|
||||
"invalidFolderName": "Имя папки содержит недопустимые символы",
|
||||
"noDragState": "Ожидающая операция перетаскивания не найдена"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "Папки не найдены",
|
||||
"dragHint": "Перетащите элементы сюда, чтобы создать папки",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Проверить обновления в этой папке",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "无法确定移动的目标路径。",
|
||||
"moveUnsupported": "此条目不支持移动。",
|
||||
"createFolderHint": "释放以创建新文件夹",
|
||||
"newFolderName": "新文件夹名称",
|
||||
"folderNameHint": "按 Enter 确认,Escape 取消",
|
||||
"emptyFolderName": "请输入文件夹名称",
|
||||
"invalidFolderName": "文件夹名称包含无效字符",
|
||||
"noDragState": "未找到待处理的拖放操作"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "未找到文件夹",
|
||||
"dragHint": "拖拽项目到此处以创建文件夹",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "检查此文件夹的更新",
|
||||
|
||||
+1
-4
@@ -1266,17 +1266,14 @@
|
||||
"dragDrop": {
|
||||
"unableToResolveRoot": "無法確定移動的目標路徑。",
|
||||
"moveUnsupported": "此項目不支援移動。",
|
||||
"createFolderHint": "放開以建立新資料夾",
|
||||
"newFolderName": "新資料夾名稱",
|
||||
"folderNameHint": "按 Enter 確認,Escape 取消",
|
||||
"emptyFolderName": "請輸入資料夾名稱",
|
||||
"invalidFolderName": "資料夾名稱包含無效字元",
|
||||
"noDragState": "未找到待處理的拖放操作"
|
||||
},
|
||||
"empty": {
|
||||
"noFolders": "未找到資料夾",
|
||||
"dragHint": "將項目拖到此處以建立資料夾",
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above, or drag items here to create folders"
|
||||
"createHint": "[TODO: Translate] Click the New Folder button above to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "檢查此資料夾的更新",
|
||||
|
||||
@@ -639,90 +639,40 @@
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Create folder drop zone */
|
||||
.sidebar-create-folder-zone {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
padding: 16px;
|
||||
border: 2px dashed oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.4);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.08);
|
||||
/* Create folder inline row: rendered inside the tree at the creation
|
||||
location, styled like a regular node row with a full-width input */
|
||||
.sidebar-create-folder-row {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sidebar-tree-node-content.sidebar-create-folder-row:hover,
|
||||
.sidebar-node-content.sidebar-create-folder-row:hover {
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-create-folder-spacer {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: var(--transition-base);
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sidebar-create-folder-zone.active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.sidebar-create-folder-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.sidebar-create-folder-row .sidebar-tree-folder-icon,
|
||||
.sidebar-create-folder-row .sidebar-folder-icon {
|
||||
color: var(--lora-accent);
|
||||
font-size: 0.85em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-create-folder-content i {
|
||||
font-size: 1.5em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Create folder input container */
|
||||
.sidebar-create-folder-input-container {
|
||||
/* Sticky footer inside the scroll container: always visible at the
|
||||
bottom of the viewport regardless of tree scroll position */
|
||||
position: sticky;
|
||||
bottom: 8px;
|
||||
margin: 8px 16px 0;
|
||||
padding: 12px;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 20;
|
||||
animation: slideUp 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-create-folder-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-create-folder-input-wrapper > i {
|
||||
color: var(--lora-accent);
|
||||
font-size: 1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sidebar-create-folder-input {
|
||||
flex: 1;
|
||||
min-width: 0; /* allow the input to shrink below its intrinsic width */
|
||||
padding: 6px 10px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.85em;
|
||||
font-size: 1em;
|
||||
outline: none;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
@@ -732,49 +682,6 @@
|
||||
box-shadow: 0 0 0 2px oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15);
|
||||
}
|
||||
|
||||
.sidebar-create-folder-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: var(--border-radius-xs);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sidebar-create-folder-btn:hover,
|
||||
.sidebar-create-folder-btn:focus-visible {
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sidebar-create-folder-confirm:hover,
|
||||
.sidebar-create-folder-confirm:focus-visible {
|
||||
background: oklch(from var(--success-color) l c h / 0.15);
|
||||
color: var(--success-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sidebar-create-folder-cancel:hover,
|
||||
.sidebar-create-folder-cancel:focus-visible {
|
||||
background: oklch(from var(--error-color) l c h / 0.15);
|
||||
color: var(--error-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sidebar-create-folder-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 0.75em;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Dragging state for sidebar */
|
||||
.folder-sidebar.dragging-active {
|
||||
border-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.5);
|
||||
@@ -786,11 +693,6 @@
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.02);
|
||||
}
|
||||
|
||||
/* Tree container positioning for create folder elements */
|
||||
.sidebar-tree-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Folder context menu - positioned relative to sidebar */
|
||||
#sidebarFolderContextMenu {
|
||||
z-index: var(--z-modal, 1002);
|
||||
|
||||
@@ -36,17 +36,16 @@ export class SidebarManager {
|
||||
this.draggedRootPath = null;
|
||||
this.draggedFromBulk = false;
|
||||
this.dragHandlersInitialized = false;
|
||||
this.sidebarDragHandlersInitialized = false;
|
||||
this.folderTreeElement = null;
|
||||
this.currentDropTarget = null;
|
||||
this.lastPageControls = null;
|
||||
this.isDisabledByPage = false;
|
||||
this.initializationPromise = null;
|
||||
this.isCreatingFolder = false;
|
||||
this._pendingDragState = null; // 用于保存拖拽创建文件夹时的状态
|
||||
this.showEmptyFolders = false;
|
||||
this.nonEmptyFolders = null; // models-only folder set used to dim empty nodes
|
||||
this._createFolderBasePath = null;
|
||||
this._createFolderTempChildren = null; // children container added for a leaf parent during inline creation
|
||||
|
||||
// Bind methods
|
||||
this.handleTreeClick = this.handleTreeClick.bind(this);
|
||||
@@ -68,10 +67,6 @@ export class SidebarManager {
|
||||
this.handleFolderDragOver = this.handleFolderDragOver.bind(this);
|
||||
this.handleFolderDragLeave = this.handleFolderDragLeave.bind(this);
|
||||
this.handleFolderDrop = this.handleFolderDrop.bind(this);
|
||||
this.handleSidebarDragEnter = this.handleSidebarDragEnter.bind(this);
|
||||
this.handleSidebarDragOver = this.handleSidebarDragOver.bind(this);
|
||||
this.handleSidebarDragLeave = this.handleSidebarDragLeave.bind(this);
|
||||
this.handleSidebarDrop = this.handleSidebarDrop.bind(this);
|
||||
this.handleCreateFolderSubmit = this.handleCreateFolderSubmit.bind(this);
|
||||
this.handleCreateFolderCancel = this.handleCreateFolderCancel.bind(this);
|
||||
this.handleHideToggle = this.handleHideToggle.bind(this);
|
||||
@@ -121,16 +116,6 @@ export class SidebarManager {
|
||||
this.resetDragState();
|
||||
this.hideCreateFolderInput();
|
||||
|
||||
// Cleanup sidebar drag handlers
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
if (sidebar && this.sidebarDragHandlersInitialized) {
|
||||
sidebar.removeEventListener('dragenter', this.handleSidebarDragEnter);
|
||||
sidebar.removeEventListener('dragover', this.handleSidebarDragOver);
|
||||
sidebar.removeEventListener('dragleave', this.handleSidebarDragLeave);
|
||||
sidebar.removeEventListener('drop', this.handleSidebarDrop);
|
||||
this.sidebarDragHandlersInitialized = false;
|
||||
}
|
||||
|
||||
this.hideSidebarHiddenIndicator();
|
||||
|
||||
// Reset state
|
||||
@@ -147,6 +132,7 @@ export class SidebarManager {
|
||||
this.showEmptyFolders = false;
|
||||
this.nonEmptyFolders = null;
|
||||
this._createFolderBasePath = null;
|
||||
this._createFolderTempChildren = null;
|
||||
|
||||
// Reset container margin
|
||||
const container = document.querySelector('.container');
|
||||
@@ -233,16 +219,6 @@ export class SidebarManager {
|
||||
|
||||
this.folderTreeElement = folderTree;
|
||||
}
|
||||
|
||||
// Add sidebar-level drag handlers for creating new folders
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
if (sidebar && !this.sidebarDragHandlersInitialized) {
|
||||
sidebar.addEventListener('dragenter', this.handleSidebarDragEnter);
|
||||
sidebar.addEventListener('dragover', this.handleSidebarDragOver);
|
||||
sidebar.addEventListener('dragleave', this.handleSidebarDragLeave);
|
||||
sidebar.addEventListener('drop', this.handleSidebarDrop);
|
||||
this.sidebarDragHandlersInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleCardDragStart(event) {
|
||||
@@ -303,7 +279,7 @@ export class SidebarManager {
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('dragging-active');
|
||||
}
|
||||
|
||||
|
||||
this.clearAllDropHighlights();
|
||||
this.resetDragState();
|
||||
}
|
||||
@@ -564,354 +540,141 @@ export class SidebarManager {
|
||||
this.draggedFromBulk = false;
|
||||
}
|
||||
|
||||
// Version of performDragMove that accepts state as parameters (for create folder submit)
|
||||
async performDragMoveWithState(targetRelativePath, draggedFilePaths, draggedRootPath, draggedFromBulk) {
|
||||
console.log('[SidebarManager] performDragMoveWithState called with:', { targetRelativePath, draggedFilePaths, draggedRootPath, draggedFromBulk });
|
||||
|
||||
if (!draggedFilePaths || draggedFilePaths.length === 0) {
|
||||
console.log('[SidebarManager] performDragMoveWithState returning false - no draggedFilePaths');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.apiClient) {
|
||||
this.apiClient = this.pageControls?.getSidebarApiClient?.()
|
||||
|| this.pageControls?.sidebarApiClient
|
||||
|| getModelApiClient();
|
||||
}
|
||||
|
||||
if (this.apiClient?.apiConfig?.config?.supportsMove === false) {
|
||||
console.log('[SidebarManager] performDragMoveWithState returning false - supportsMove is false');
|
||||
showToast('toast.models.moveFailed', { message: translate('sidebar.dragDrop.moveUnsupported', {}, 'Move not supported for this page') }, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootPath = draggedRootPath ? draggedRootPath.replace(/\\/g, '/') : '';
|
||||
console.log('[SidebarManager] rootPath:', rootPath);
|
||||
if (!rootPath) {
|
||||
console.log('[SidebarManager] performDragMoveWithState returning false - no rootPath');
|
||||
showToast(
|
||||
'toast.models.moveFailed',
|
||||
{ message: translate('sidebar.dragDrop.unableToResolveRoot', {}, 'Unable to determine destination path for move.') },
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const destination = this.combineRootAndRelativePath(rootPath, targetRelativePath);
|
||||
const useBulkMove = draggedFromBulk || draggedFilePaths.length > 1;
|
||||
|
||||
try {
|
||||
console.log('[SidebarManager] calling apiClient.move, useBulkMove:', useBulkMove);
|
||||
let movedFiles = []; // Array of { original_file_path, new_file_path }
|
||||
|
||||
if (useBulkMove) {
|
||||
const results = await this.apiClient.moveBulkModels(draggedFilePaths, destination);
|
||||
movedFiles = (results || [])
|
||||
.filter(r => r.success)
|
||||
.map(r => ({ original_file_path: r.original_file_path, new_file_path: r.new_file_path }));
|
||||
} else {
|
||||
const result = await this.apiClient.moveSingleModel(draggedFilePaths[0], destination);
|
||||
if (result) {
|
||||
movedFiles.push({
|
||||
original_file_path: result.original_file_path || draggedFilePaths[0],
|
||||
new_file_path: result.new_file_path
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log('[SidebarManager] apiClient.move successful');
|
||||
|
||||
// Update VirtualScroller in-place instead of full reload
|
||||
if (movedFiles.length > 0 && state.virtualScroller) {
|
||||
const pageState = getCurrentPageState();
|
||||
const normalizedActive = (pageState.activeFolder || '').replace(/\\/g, '/').replace(/\/$/, '');
|
||||
const isRecursive = pageState.searchOptions?.recursive ?? true;
|
||||
const isFolderFiltered = pageState.activeFolder !== null;
|
||||
|
||||
const normalizedTarget = targetRelativePath.replace(/\\/g, '/').replace(/\/$/, '');
|
||||
|
||||
// Determine if items in the target folder are visible in the current view
|
||||
let itemsRemainVisible = true;
|
||||
if (isFolderFiltered) {
|
||||
if (isRecursive) {
|
||||
itemsRemainVisible = normalizedActive === '' ||
|
||||
normalizedTarget === normalizedActive ||
|
||||
normalizedTarget.startsWith(normalizedActive + '/');
|
||||
} else {
|
||||
itemsRemainVisible = normalizedTarget === normalizedActive;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsRemainVisible) {
|
||||
// Items stay visible — update each item's file_path to reflect new location
|
||||
for (const moved of movedFiles) {
|
||||
if (moved.original_file_path && moved.new_file_path) {
|
||||
state.virtualScroller.updateSingleItem(moved.original_file_path, {
|
||||
file_path: moved.new_file_path,
|
||||
folder: normalizedTarget
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Items no longer visible in current folder — remove from VirtualScroller
|
||||
const pathsToRemove = movedFiles
|
||||
.map(m => m.original_file_path)
|
||||
.filter(Boolean);
|
||||
if (pathsToRemove.length > 0) {
|
||||
state.virtualScroller.removeMultipleItemsByFilePath(pathsToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh sidebar folder tree only (no model data reload)
|
||||
await this.refresh();
|
||||
|
||||
if (draggedFromBulk && state.bulkMode && typeof bulkManager?.toggleBulkMode === 'function') {
|
||||
bulkManager.toggleBulkMode();
|
||||
}
|
||||
|
||||
console.log('[SidebarManager] performDragMoveWithState returning true');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[SidebarManager] Error moving model(s) via drag-and-drop:', error);
|
||||
showToast('toast.models.moveFailed', { message: error.message || 'Unknown error' }, 'error');
|
||||
console.log('[SidebarManager] performDragMoveWithState returning false due to error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Sidebar-level drag handlers for creating new folders =====
|
||||
|
||||
handleSidebarDragEnter(event) {
|
||||
if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return;
|
||||
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
// Only show create folder zone if not hovering over an existing folder
|
||||
const folderElement = this.getFolderElementFromEvent(event);
|
||||
if (folderElement) {
|
||||
this.hideCreateFolderZone();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if drag is within the sidebar tree container area
|
||||
const treeContainer = document.querySelector('.sidebar-tree-container');
|
||||
if (treeContainer && treeContainer.contains(event.target)) {
|
||||
event.preventDefault();
|
||||
this.showCreateFolderZone();
|
||||
}
|
||||
}
|
||||
|
||||
handleSidebarDragOver(event) {
|
||||
if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return;
|
||||
|
||||
const folderElement = this.getFolderElementFromEvent(event);
|
||||
if (folderElement) {
|
||||
this.hideCreateFolderZone();
|
||||
return;
|
||||
}
|
||||
|
||||
const treeContainer = document.querySelector('.sidebar-tree-container');
|
||||
if (treeContainer && treeContainer.contains(event.target)) {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSidebarDragLeave(event) {
|
||||
if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return;
|
||||
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
const relatedTarget = event.relatedTarget instanceof Element ? event.relatedTarget : null;
|
||||
|
||||
// Only hide if leaving the sidebar entirely
|
||||
if (!relatedTarget || !sidebar.contains(relatedTarget)) {
|
||||
this.hideCreateFolderZone();
|
||||
}
|
||||
}
|
||||
|
||||
async handleSidebarDrop(event) {
|
||||
if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return;
|
||||
|
||||
const folderElement = this.getFolderElementFromEvent(event);
|
||||
if (folderElement) {
|
||||
// Let the folder drop handler take over
|
||||
return;
|
||||
}
|
||||
|
||||
const treeContainer = document.querySelector('.sidebar-tree-container');
|
||||
if (!treeContainer || !treeContainer.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Show create folder input
|
||||
this.showCreateFolderInput();
|
||||
}
|
||||
|
||||
showCreateFolderZone() {
|
||||
if (this.isCreatingFolder) return;
|
||||
|
||||
const treeContainer = document.querySelector('.sidebar-tree-container');
|
||||
if (!treeContainer) return;
|
||||
|
||||
let zone = document.getElementById('sidebarCreateFolderZone');
|
||||
if (!zone) {
|
||||
zone = document.createElement('div');
|
||||
zone.id = 'sidebarCreateFolderZone';
|
||||
zone.className = 'sidebar-create-folder-zone';
|
||||
zone.innerHTML = `
|
||||
<div class="sidebar-create-folder-content">
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
<span>${translate('sidebar.dragDrop.createFolderHint', {}, 'Release to create new folder')}</span>
|
||||
</div>
|
||||
`;
|
||||
treeContainer.appendChild(zone);
|
||||
}
|
||||
|
||||
zone.classList.add('active');
|
||||
}
|
||||
|
||||
hideCreateFolderZone() {
|
||||
const zone = document.getElementById('sidebarCreateFolderZone');
|
||||
if (zone) {
|
||||
zone.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
showCreateFolderInput(basePath = null) {
|
||||
console.log('[SidebarManager] showCreateFolderInput called');
|
||||
// Remove any existing input first — hideCreateFolderInput() also
|
||||
// clears isCreatingFolder, so it must run before the flag is set.
|
||||
this.hideCreateFolderInput();
|
||||
this.isCreatingFolder = true;
|
||||
|
||||
// The folder is created under the given base path; falls back to the
|
||||
// currently selected folder (drag-and-drop flow) or the root.
|
||||
// currently selected folder or the root.
|
||||
this._createFolderBasePath = basePath !== null ? basePath : (this.selectedPath || '');
|
||||
|
||||
// 立即保存拖拽状态,防止后续事件(如blur)清空状态
|
||||
this._pendingDragState = this.draggedFilePaths && this.draggedFilePaths.length > 0 ? {
|
||||
filePaths: [...this.draggedFilePaths],
|
||||
rootPath: this.draggedRootPath,
|
||||
fromBulk: this.draggedFromBulk
|
||||
} : null;
|
||||
console.log('[SidebarManager] saved pending drag state:', this._pendingDragState);
|
||||
|
||||
this.hideCreateFolderZone();
|
||||
|
||||
const treeContainer = document.querySelector('.sidebar-tree-container');
|
||||
if (!treeContainer) return;
|
||||
|
||||
// Remove existing input if any
|
||||
this.hideCreateFolderInput();
|
||||
|
||||
const inputContainer = document.createElement('div');
|
||||
inputContainer.id = 'sidebarCreateFolderInput';
|
||||
inputContainer.className = 'sidebar-create-folder-input-container';
|
||||
inputContainer.innerHTML = `
|
||||
<div class="sidebar-create-folder-input-wrapper">
|
||||
<i class="fas fa-folder-plus"></i>
|
||||
<input type="text"
|
||||
class="sidebar-create-folder-input"
|
||||
placeholder="${translate('sidebar.dragDrop.newFolderName', {}, 'New folder name')}"
|
||||
autofocus />
|
||||
<button class="sidebar-create-folder-btn sidebar-create-folder-confirm" title="${translate('common.confirm', {}, 'Confirm')}">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
<button class="sidebar-create-folder-btn sidebar-create-folder-cancel" title="${translate('common.cancel', {}, 'Cancel')}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sidebar-create-folder-hint">
|
||||
${translate('sidebar.dragDrop.folderNameHint', {}, 'Press Enter to confirm, Escape to cancel')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
treeContainer.appendChild(inputContainer);
|
||||
|
||||
// Focus input
|
||||
const input = inputContainer.querySelector('.sidebar-create-folder-input');
|
||||
if (input) {
|
||||
input.focus();
|
||||
const folderTree = document.getElementById('sidebarFolderTree');
|
||||
if (!folderTree) {
|
||||
this.isCreatingFolder = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind events
|
||||
const confirmBtn = inputContainer.querySelector('.sidebar-create-folder-confirm');
|
||||
const cancelBtn = inputContainer.querySelector('.sidebar-create-folder-cancel');
|
||||
// Inline row at the creation location, file-explorer style: the new
|
||||
// folder will appear exactly where the input row is shown.
|
||||
const row = this._buildCreateFolderRow();
|
||||
this._insertCreateFolderRow(folderTree, row);
|
||||
row.scrollIntoView?.({ block: 'nearest' });
|
||||
|
||||
// Flag to prevent blur from canceling when clicking buttons
|
||||
let isButtonClick = false;
|
||||
const input = row.querySelector('.sidebar-create-folder-input');
|
||||
if (!input) return;
|
||||
input.focus();
|
||||
|
||||
confirmBtn?.addEventListener('mousedown', () => {
|
||||
isButtonClick = true;
|
||||
console.log('[SidebarManager] confirmBtn mousedown - isButtonClick set to true');
|
||||
});
|
||||
cancelBtn?.addEventListener('mousedown', () => {
|
||||
isButtonClick = true;
|
||||
console.log('[SidebarManager] cancelBtn mousedown - isButtonClick set to true');
|
||||
});
|
||||
|
||||
confirmBtn?.addEventListener('click', (e) => {
|
||||
console.log('[SidebarManager] confirmBtn click event triggered');
|
||||
this.handleCreateFolderSubmit();
|
||||
});
|
||||
cancelBtn?.addEventListener('click', () => {
|
||||
console.log('[SidebarManager] cancelBtn click event triggered');
|
||||
this.handleCreateFolderCancel();
|
||||
});
|
||||
input?.addEventListener('keydown', (e) => {
|
||||
console.log('[SidebarManager] input keydown:', e.key);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
console.log('[SidebarManager] Enter pressed, calling handleCreateFolderSubmit');
|
||||
this.handleCreateFolderSubmit();
|
||||
} else if (e.key === 'Escape') {
|
||||
console.log('[SidebarManager] Escape pressed, calling handleCreateFolderCancel');
|
||||
this.handleCreateFolderCancel();
|
||||
}
|
||||
});
|
||||
input?.addEventListener('blur', () => {
|
||||
console.log('[SidebarManager] input blur event - isButtonClick:', isButtonClick);
|
||||
// Delay to allow button clicks to process first
|
||||
// Clicking away cancels creation, mirroring file-explorer behavior
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => {
|
||||
console.log('[SidebarManager] blur timeout - isButtonClick:', isButtonClick, 'activeElement:', document.activeElement?.className);
|
||||
if (!isButtonClick && document.activeElement !== confirmBtn && document.activeElement !== cancelBtn) {
|
||||
console.log('[SidebarManager] blur timeout - calling handleCreateFolderCancel');
|
||||
if (this.isCreatingFolder) {
|
||||
this.handleCreateFolderCancel();
|
||||
} else {
|
||||
console.log('[SidebarManager] blur timeout - NOT canceling (button click detected)');
|
||||
}
|
||||
isButtonClick = false;
|
||||
}, 200);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
hideCreateFolderInput() {
|
||||
console.log('[SidebarManager] hideCreateFolderInput called');
|
||||
const inputContainer = document.getElementById('sidebarCreateFolderInput');
|
||||
console.log('[SidebarManager] inputContainer:', inputContainer);
|
||||
if (inputContainer) {
|
||||
inputContainer.remove();
|
||||
console.log('[SidebarManager] inputContainer removed');
|
||||
_buildCreateFolderRow() {
|
||||
const isListMode = this.displayMode === 'list';
|
||||
const row = document.createElement('div');
|
||||
row.id = 'sidebarCreateFolderInput';
|
||||
row.className = 'sidebar-create-folder-node';
|
||||
row.innerHTML = `
|
||||
<div class="${isListMode ? 'sidebar-node-content' : 'sidebar-tree-node-content'} sidebar-create-folder-row">
|
||||
${isListMode ? '' : `
|
||||
<div class="sidebar-tree-expand-icon sidebar-create-folder-spacer">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</div>`}
|
||||
<i class="fas fa-folder-plus sidebar-tree-folder-icon"></i>
|
||||
<input type="text"
|
||||
class="sidebar-create-folder-input"
|
||||
placeholder="${translate('sidebar.dragDrop.newFolderName', {}, 'New folder name')}" />
|
||||
</div>
|
||||
`;
|
||||
return row;
|
||||
}
|
||||
|
||||
_insertCreateFolderRow(folderTree, row) {
|
||||
const parentPath = this._createFolderBasePath;
|
||||
|
||||
if (this.displayMode === 'list') {
|
||||
if (parentPath) {
|
||||
const parentItem = [...folderTree.querySelectorAll('.sidebar-folder-item')]
|
||||
.find(item => item.dataset.path === parentPath);
|
||||
if (parentItem) {
|
||||
parentItem.after(row);
|
||||
return;
|
||||
}
|
||||
}
|
||||
folderTree.appendChild(row);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentPath) {
|
||||
// Expand the parent so the row is visible, then insert as its
|
||||
// first child.
|
||||
if (!this.expandedNodes.has(parentPath)) {
|
||||
this.expandedNodes.add(parentPath);
|
||||
this.saveExpandedState();
|
||||
this.renderTree();
|
||||
}
|
||||
const parentNode = [...folderTree.querySelectorAll('.sidebar-tree-node')]
|
||||
.find(node => node.dataset.path === parentPath);
|
||||
if (parentNode) {
|
||||
let children = parentNode.querySelector(':scope > .sidebar-tree-children');
|
||||
if (!children) {
|
||||
// Leaf folder: renderTree() only creates a children
|
||||
// container for nodes with subfolders, so add one.
|
||||
children = document.createElement('div');
|
||||
children.className = 'sidebar-tree-children expanded';
|
||||
parentNode.appendChild(children);
|
||||
this._createFolderTempChildren = children;
|
||||
}
|
||||
children.prepend(row);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Root creation (or parent not currently visible): append at top level
|
||||
folderTree.appendChild(row);
|
||||
}
|
||||
|
||||
hideCreateFolderInput() {
|
||||
// Clear the flag first so the input's blur handler does not treat
|
||||
// removing the row as a cancel.
|
||||
this.isCreatingFolder = false;
|
||||
console.log('[SidebarManager] isCreatingFolder set to false');
|
||||
|
||||
const row = document.getElementById('sidebarCreateFolderInput');
|
||||
if (row) {
|
||||
row.remove();
|
||||
}
|
||||
|
||||
// Remove the temporary children container if it is still empty
|
||||
if (this._createFolderTempChildren) {
|
||||
const container = this._createFolderTempChildren;
|
||||
this._createFolderTempChildren = null;
|
||||
if (container.isConnected && container.children.length === 0) {
|
||||
container.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleCreateFolderSubmit() {
|
||||
console.log('[SidebarManager] handleCreateFolderSubmit called');
|
||||
const input = document.querySelector('#sidebarCreateFolderInput .sidebar-create-folder-input');
|
||||
console.log('[SidebarManager] input element:', input);
|
||||
if (!input) {
|
||||
console.log('[SidebarManager] input not found, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
const folderName = input.value.trim();
|
||||
console.log('[SidebarManager] folderName:', folderName);
|
||||
if (!folderName) {
|
||||
showToast('sidebar.dragDrop.emptyFolderName', {}, 'warning');
|
||||
return;
|
||||
@@ -927,41 +690,10 @@ export class SidebarManager {
|
||||
// opened (context-menu folder or current selection), or root
|
||||
const parentPath = this._createFolderBasePath || '';
|
||||
const targetRelativePath = parentPath ? `${parentPath}/${folderName}` : folderName;
|
||||
console.log('[SidebarManager] targetRelativePath:', targetRelativePath);
|
||||
|
||||
// 使用 showCreateFolderInput 时保存的拖拽状态
|
||||
const pendingState = this._pendingDragState;
|
||||
console.log('[SidebarManager] using pending drag state:', pendingState);
|
||||
|
||||
this.hideCreateFolderInput();
|
||||
|
||||
if (!pendingState || !pendingState.filePaths || pendingState.filePaths.length === 0) {
|
||||
// Standalone creation (header button / context menu): create an
|
||||
// empty folder on disk instead of moving dragged models into it.
|
||||
await this._createFolder(targetRelativePath, parentPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the move with saved state
|
||||
console.log('[SidebarManager] calling performDragMove with pending state');
|
||||
const success = await this.performDragMoveWithState(targetRelativePath, pendingState.filePaths, pendingState.rootPath, pendingState.fromBulk);
|
||||
console.log('[SidebarManager] performDragMove result:', success);
|
||||
|
||||
if (success) {
|
||||
// Expand the parent folder to show the new folder
|
||||
if (parentPath) {
|
||||
this.expandedNodes.add(parentPath);
|
||||
this.saveExpandedState();
|
||||
}
|
||||
// Refresh the tree to show the newly created folder
|
||||
// restoreSelectedFolder() inside refresh() will maintain the current active folder
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
// 清理待处理的拖拽状态
|
||||
this._pendingDragState = null;
|
||||
this.resetDragState();
|
||||
this.clearAllDropHighlights();
|
||||
await this._createFolder(targetRelativePath, parentPath);
|
||||
}
|
||||
|
||||
async _createFolder(targetRelativePath, parentPath) {
|
||||
@@ -1025,10 +757,6 @@ export class SidebarManager {
|
||||
|
||||
handleCreateFolderCancel() {
|
||||
this.hideCreateFolderInput();
|
||||
// 清理待处理的拖拽状态
|
||||
this._pendingDragState = null;
|
||||
this.resetDragState();
|
||||
this.clearAllDropHighlights();
|
||||
}
|
||||
|
||||
saveSelectedFolder() {
|
||||
@@ -1399,20 +1127,18 @@ export class SidebarManager {
|
||||
const folderTree = document.getElementById('sidebarFolderTree');
|
||||
if (!folderTree) return;
|
||||
|
||||
// Pages with folder management (model libraries) also offer the
|
||||
// header create button; recipes can only create folders via drag.
|
||||
const hintKey = this._supportsFolderManagement()
|
||||
? 'sidebar.empty.createHint'
|
||||
: 'sidebar.empty.dragHint';
|
||||
// Only pages with folder management (model libraries) offer the
|
||||
// header create button; other pages just show the empty label.
|
||||
const hintHtml = this._supportsFolderManagement() ? `
|
||||
<div class="sidebar-empty-hint">
|
||||
<i class="fas fa-hand-pointer"></i>
|
||||
${translate('sidebar.empty.createHint', {}, 'Click the New Folder button above to create folders')}
|
||||
</div>` : '';
|
||||
|
||||
folderTree.innerHTML = `
|
||||
<div class="sidebar-tree-placeholder">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<div>${translate('sidebar.empty.noFolders', {}, 'No folders found')}</div>
|
||||
<div class="sidebar-empty-hint">
|
||||
<i class="fas fa-hand-pointer"></i>
|
||||
${translate(hintKey, {}, 'Drag items here to create folders')}
|
||||
</div>
|
||||
<div>${translate('sidebar.empty.noFolders', {}, 'No folders found')}</div>${hintHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1448,6 +1174,9 @@ export class SidebarManager {
|
||||
}
|
||||
|
||||
handleTreeClick(event) {
|
||||
// Clicks on the inline create-folder row must not select/toggle nodes
|
||||
if (event.target.closest('.sidebar-create-folder-node')) return;
|
||||
|
||||
if (this.displayMode === 'list') {
|
||||
this.handleFolderListClick(event);
|
||||
return;
|
||||
@@ -1482,6 +1211,9 @@ export class SidebarManager {
|
||||
}
|
||||
|
||||
handleTreeContextMenu(event) {
|
||||
// No context menu on the inline create-folder row
|
||||
if (event.target.closest('.sidebar-create-folder-node')) return;
|
||||
|
||||
const nodeContent = event.target.closest('.sidebar-tree-node, .sidebar-folder-item');
|
||||
if (!nodeContent) return;
|
||||
|
||||
|
||||
@@ -332,22 +332,124 @@ describe('SidebarManager folder creation', () => {
|
||||
expect(manager.refresh).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens the create-folder input for a context-menu folder', () => {
|
||||
it('opens the create-folder input as an inline row under the context-menu folder', () => {
|
||||
const manager = createManager(createApiClient());
|
||||
document.body.innerHTML = '<div class="sidebar-tree-container"></div>';
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
manager.treeData = { characters: {} };
|
||||
manager.renderTree();
|
||||
|
||||
manager._performFolderAction('create-subfolder', 'characters');
|
||||
|
||||
const input = document.querySelector('#sidebarCreateFolderInput .sidebar-create-folder-input');
|
||||
expect(input).not.toBeNull();
|
||||
const row = document.getElementById('sidebarCreateFolderInput');
|
||||
expect(row).not.toBeNull();
|
||||
expect(row.classList.contains('sidebar-create-folder-node')).toBe(true);
|
||||
expect(manager._createFolderBasePath).toBe('characters');
|
||||
// The leaf parent is expanded and the row sits inside its children container
|
||||
expect(manager.expandedNodes.has('characters')).toBe(true);
|
||||
const parentNode = document.querySelector('.sidebar-tree-node[data-path="characters"]');
|
||||
expect(parentNode.querySelector(':scope > .sidebar-tree-children').contains(row)).toBe(true);
|
||||
});
|
||||
|
||||
it('inserts the row as the first child of an already-expanded parent', () => {
|
||||
const manager = createManager(createApiClient());
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
manager.treeData = { characters: { anime: {} } };
|
||||
manager.expandedNodes = new Set(['characters']);
|
||||
manager.renderTree();
|
||||
|
||||
manager.showCreateFolderInput('characters');
|
||||
|
||||
const children = document.querySelector('.sidebar-tree-node[data-path="characters"] > .sidebar-tree-children');
|
||||
expect(children.firstElementChild.id).toBe('sidebarCreateFolderInput');
|
||||
// Existing children container is reused, no temporary one is tracked
|
||||
expect(manager._createFolderTempChildren).toBeNull();
|
||||
});
|
||||
|
||||
it('appends the row at the top level for root creation', () => {
|
||||
const manager = createManager(createApiClient());
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
manager.treeData = { characters: {} };
|
||||
manager.renderTree();
|
||||
|
||||
manager.showCreateFolderInput('');
|
||||
|
||||
const folderTree = document.getElementById('sidebarFolderTree');
|
||||
const row = document.getElementById('sidebarCreateFolderInput');
|
||||
expect(row.parentElement).toBe(folderTree);
|
||||
expect(folderTree.lastElementChild).toBe(row);
|
||||
});
|
||||
|
||||
it('inserts the row after the parent item in list mode', () => {
|
||||
const manager = createManager(createApiClient(), { displayMode: 'list' });
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
manager.foldersList = ['characters', 'characters/anime'];
|
||||
manager.renderFolderList();
|
||||
|
||||
manager.showCreateFolderInput('characters');
|
||||
|
||||
const items = [...document.querySelectorAll('#sidebarFolderTree > div')];
|
||||
const parentIndex = items.findIndex(el => el.dataset.path === 'characters');
|
||||
expect(items[parentIndex + 1].id).toBe('sidebarCreateFolderInput');
|
||||
// List-mode rows use the list content styling, not the tree one
|
||||
expect(items[parentIndex + 1].querySelector('.sidebar-node-content')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('removes the temporary children container when creation is canceled', () => {
|
||||
const manager = createManager(createApiClient());
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
manager.treeData = { characters: {} };
|
||||
manager.renderTree();
|
||||
|
||||
manager.showCreateFolderInput('characters');
|
||||
manager.handleCreateFolderCancel();
|
||||
|
||||
expect(document.getElementById('sidebarCreateFolderInput')).toBeNull();
|
||||
expect(manager.isCreatingFolder).toBe(false);
|
||||
const parentNode = document.querySelector('.sidebar-tree-node[data-path="characters"]');
|
||||
expect(parentNode.querySelector(':scope > .sidebar-tree-children')).toBeNull();
|
||||
});
|
||||
|
||||
it('cancels creation when the input loses focus', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const manager = createManager(createApiClient());
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
|
||||
manager.showCreateFolderInput('');
|
||||
const input = document.querySelector('#sidebarCreateFolderInput .sidebar-create-folder-input');
|
||||
input.dispatchEvent(new Event('blur'));
|
||||
vi.advanceTimersByTime(150);
|
||||
|
||||
expect(document.getElementById('sidebarCreateFolderInput')).toBeNull();
|
||||
expect(manager.isCreatingFolder).toBe(false);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('ignores tree clicks and context menus on the create row', () => {
|
||||
const manager = createManager(createApiClient());
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
manager.treeData = { characters: {} };
|
||||
manager.renderTree();
|
||||
manager.selectFolder = vi.fn();
|
||||
const showMenu = vi.spyOn(manager, '_showFolderContextMenu').mockImplementation(() => {});
|
||||
|
||||
manager.showCreateFolderInput('characters');
|
||||
const input = document.querySelector('#sidebarCreateFolderInput .sidebar-create-folder-input');
|
||||
|
||||
manager.handleTreeClick({ target: input });
|
||||
expect(manager.selectFolder).not.toHaveBeenCalled();
|
||||
|
||||
manager.handleTreeContextMenu({ target: input, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
expect(showMenu).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits a standalone folder creation when no drag is pending', async () => {
|
||||
const apiClient = createApiClient();
|
||||
const manager = createManager(apiClient);
|
||||
manager.refresh = vi.fn().mockResolvedValue(undefined);
|
||||
document.body.innerHTML = '<div class="sidebar-tree-container"></div>';
|
||||
document.body.innerHTML = '<div id="sidebarFolderTree"></div>';
|
||||
|
||||
manager.showCreateFolderInput('characters');
|
||||
document.querySelector('#sidebarCreateFolderInput .sidebar-create-folder-input').value = 'anime';
|
||||
@@ -358,3 +460,4 @@ describe('SidebarManager folder creation', () => {
|
||||
expect(document.getElementById('sidebarCreateFolderInput')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user