mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Add recursive root folder scanning with API and UI updates. fixes #737
This commit is contained in:
@@ -1267,13 +1267,13 @@ class RecipeScanner:
|
|||||||
# Skip further filtering if we're only filtering by LoRA hash with bypass enabled
|
# Skip further filtering if we're only filtering by LoRA hash with bypass enabled
|
||||||
if not (lora_hash and bypass_filters):
|
if not (lora_hash and bypass_filters):
|
||||||
# Apply folder filter before other criteria
|
# Apply folder filter before other criteria
|
||||||
normalized_folder = (folder or "").strip("/")
|
if folder is not None:
|
||||||
if normalized_folder:
|
normalized_folder = folder.strip("/")
|
||||||
def matches_folder(item_folder: str) -> bool:
|
def matches_folder(item_folder: str) -> bool:
|
||||||
item_path = (item_folder or "").strip("/")
|
item_path = (item_folder or "").strip("/")
|
||||||
if not item_path:
|
|
||||||
return False
|
|
||||||
if recursive:
|
if recursive:
|
||||||
|
if not normalized_folder:
|
||||||
|
return True
|
||||||
return item_path == normalized_folder or item_path.startswith(f"{normalized_folder}/")
|
return item_path == normalized_folder or item_path.startswith(f"{normalized_folder}/")
|
||||||
return item_path == normalized_folder
|
return item_path == normalized_folder
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
|||||||
params.append('favorite', 'true');
|
params.append('favorite', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageState.activeFolder) {
|
if (pageState.activeFolder !== null && pageState.activeFolder !== undefined) {
|
||||||
params.append('folder', pageState.activeFolder);
|
params.append('folder', pageState.activeFolder);
|
||||||
params.append('recursive', pageState.searchOptions?.recursive !== false);
|
params.append('recursive', pageState.searchOptions?.recursive !== false);
|
||||||
} else if (pageState.searchOptions?.recursive !== undefined) {
|
} else if (pageState.searchOptions?.recursive !== undefined) {
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ export class SidebarManager {
|
|||||||
this.apiClient = pageControls?.getSidebarApiClient?.()
|
this.apiClient = pageControls?.getSidebarApiClient?.()
|
||||||
|| pageControls?.sidebarApiClient
|
|| pageControls?.sidebarApiClient
|
||||||
|| getModelApiClient();
|
|| getModelApiClient();
|
||||||
|
|
||||||
// Set initial sidebar state immediately (hidden by default)
|
// Set initial sidebar state immediately (hidden by default)
|
||||||
this.setInitialSidebarState();
|
this.setInitialSidebarState();
|
||||||
|
|
||||||
this.setupEventHandlers();
|
this.setupEventHandlers();
|
||||||
this.initializeDragAndDrop();
|
this.initializeDragAndDrop();
|
||||||
this.updateSidebarTitle();
|
this.updateSidebarTitle();
|
||||||
@@ -94,13 +94,13 @@ export class SidebarManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.restoreSelectedFolder();
|
this.restoreSelectedFolder();
|
||||||
|
|
||||||
// Apply final state with animation after everything is loaded
|
// Apply final state with animation after everything is loaded
|
||||||
this.applyFinalSidebarState();
|
this.applyFinalSidebarState();
|
||||||
|
|
||||||
// Update container margin based on initial sidebar state
|
// Update container margin based on initial sidebar state
|
||||||
this.updateContainerMargin();
|
this.updateContainerMargin();
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
console.log(`SidebarManager initialized for ${this.pageType} page`);
|
console.log(`SidebarManager initialized for ${this.pageType} page`);
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ export class SidebarManager {
|
|||||||
clearTimeout(this.hoverTimeout);
|
clearTimeout(this.hoverTimeout);
|
||||||
this.hoverTimeout = null;
|
this.hoverTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up event handlers
|
// Clean up event handlers
|
||||||
this.removeEventHandlers();
|
this.removeEventHandlers();
|
||||||
|
|
||||||
@@ -143,13 +143,13 @@ export class SidebarManager {
|
|||||||
this.apiClient = null;
|
this.apiClient = null;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.recursiveSearchEnabled = true;
|
this.recursiveSearchEnabled = true;
|
||||||
|
|
||||||
// Reset container margin
|
// Reset container margin
|
||||||
const container = document.querySelector('.container');
|
const container = document.querySelector('.container');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style.marginLeft = '';
|
container.style.marginLeft = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove resize event listener
|
// Remove resize event listener
|
||||||
window.removeEventListener('resize', this.updateContainerMargin);
|
window.removeEventListener('resize', this.updateContainerMargin);
|
||||||
|
|
||||||
@@ -191,10 +191,10 @@ export class SidebarManager {
|
|||||||
hoverArea.removeEventListener('mouseenter', this.handleHoverAreaEnter);
|
hoverArea.removeEventListener('mouseenter', this.handleHoverAreaEnter);
|
||||||
hoverArea.removeEventListener('mouseleave', this.handleHoverAreaLeave);
|
hoverArea.removeEventListener('mouseleave', this.handleHoverAreaLeave);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove document click handler
|
// Remove document click handler
|
||||||
document.removeEventListener('click', this.handleDocumentClick);
|
document.removeEventListener('click', this.handleDocumentClick);
|
||||||
|
|
||||||
// Remove resize event handler
|
// Remove resize event handler
|
||||||
window.removeEventListener('resize', this.updateContainerMargin);
|
window.removeEventListener('resize', this.updateContainerMargin);
|
||||||
|
|
||||||
@@ -486,20 +486,20 @@ export class SidebarManager {
|
|||||||
this.apiClient = this.pageControls?.getSidebarApiClient?.()
|
this.apiClient = this.pageControls?.getSidebarApiClient?.()
|
||||||
|| this.pageControls?.sidebarApiClient
|
|| this.pageControls?.sidebarApiClient
|
||||||
|| getModelApiClient();
|
|| getModelApiClient();
|
||||||
|
|
||||||
// Set initial sidebar state immediately (hidden by default)
|
// Set initial sidebar state immediately (hidden by default)
|
||||||
this.setInitialSidebarState();
|
this.setInitialSidebarState();
|
||||||
|
|
||||||
this.setupEventHandlers();
|
this.setupEventHandlers();
|
||||||
this.initializeDragAndDrop();
|
this.initializeDragAndDrop();
|
||||||
this.updateSidebarTitle();
|
this.updateSidebarTitle();
|
||||||
this.restoreSidebarState();
|
this.restoreSidebarState();
|
||||||
await this.loadFolderTree();
|
await this.loadFolderTree();
|
||||||
this.restoreSelectedFolder();
|
this.restoreSelectedFolder();
|
||||||
|
|
||||||
// Apply final state with animation after everything is loaded
|
// Apply final state with animation after everything is loaded
|
||||||
this.applyFinalSidebarState();
|
this.applyFinalSidebarState();
|
||||||
|
|
||||||
// Update container margin based on initial sidebar state
|
// Update container margin based on initial sidebar state
|
||||||
this.updateContainerMargin();
|
this.updateContainerMargin();
|
||||||
}
|
}
|
||||||
@@ -511,11 +511,11 @@ export class SidebarManager {
|
|||||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||||
|
|
||||||
if (!sidebar || !hoverArea) return;
|
if (!sidebar || !hoverArea) return;
|
||||||
|
|
||||||
// Get stored pin state
|
// Get stored pin state
|
||||||
const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, true);
|
const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, true);
|
||||||
this.isPinned = isPinned;
|
this.isPinned = isPinned;
|
||||||
|
|
||||||
// Sidebar starts hidden by default (CSS handles this)
|
// Sidebar starts hidden by default (CSS handles this)
|
||||||
// Just set up the hover area state
|
// Just set up the hover area state
|
||||||
if (window.innerWidth <= 1024) {
|
if (window.innerWidth <= 1024) {
|
||||||
@@ -583,12 +583,12 @@ export class SidebarManager {
|
|||||||
// Hover detection for auto-hide
|
// Hover detection for auto-hide
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||||
|
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.addEventListener('mouseenter', this.handleMouseEnter);
|
sidebar.addEventListener('mouseenter', this.handleMouseEnter);
|
||||||
sidebar.addEventListener('mouseleave', this.handleMouseLeave);
|
sidebar.addEventListener('mouseleave', this.handleMouseLeave);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoverArea) {
|
if (hoverArea) {
|
||||||
hoverArea.addEventListener('mouseenter', this.handleHoverAreaEnter);
|
hoverArea.addEventListener('mouseenter', this.handleHoverAreaEnter);
|
||||||
hoverArea.addEventListener('mouseleave', this.handleHoverAreaLeave);
|
hoverArea.addEventListener('mouseleave', this.handleHoverAreaLeave);
|
||||||
@@ -598,7 +598,7 @@ export class SidebarManager {
|
|||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (window.innerWidth <= 1024 && this.isVisible) {
|
if (window.innerWidth <= 1024 && this.isVisible) {
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
|
|
||||||
if (sidebar && !sidebar.contains(e.target)) {
|
if (sidebar && !sidebar.contains(e.target)) {
|
||||||
this.hideSidebar();
|
this.hideSidebar();
|
||||||
}
|
}
|
||||||
@@ -613,7 +613,7 @@ export class SidebarManager {
|
|||||||
|
|
||||||
// Add document click handler for closing dropdowns
|
// Add document click handler for closing dropdowns
|
||||||
document.addEventListener('click', this.handleDocumentClick);
|
document.addEventListener('click', this.handleDocumentClick);
|
||||||
|
|
||||||
// Add dedicated resize listener for container margin updates
|
// Add dedicated resize listener for container margin updates
|
||||||
window.addEventListener('resize', this.updateContainerMargin);
|
window.addEventListener('resize', this.updateContainerMargin);
|
||||||
|
|
||||||
@@ -660,7 +660,7 @@ export class SidebarManager {
|
|||||||
clearTimeout(this.hoverTimeout);
|
clearTimeout(this.hoverTimeout);
|
||||||
this.hoverTimeout = null;
|
this.hoverTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isPinned) {
|
if (!this.isPinned) {
|
||||||
this.showSidebar();
|
this.showSidebar();
|
||||||
}
|
}
|
||||||
@@ -710,9 +710,9 @@ export class SidebarManager {
|
|||||||
|
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||||
|
|
||||||
if (!sidebar || !hoverArea) return;
|
if (!sidebar || !hoverArea) return;
|
||||||
|
|
||||||
if (window.innerWidth <= 1024) {
|
if (window.innerWidth <= 1024) {
|
||||||
// Mobile: always use collapsed state
|
// Mobile: always use collapsed state
|
||||||
sidebar.classList.remove('auto-hide', 'hover-active', 'visible');
|
sidebar.classList.remove('auto-hide', 'hover-active', 'visible');
|
||||||
@@ -730,7 +730,7 @@ export class SidebarManager {
|
|||||||
sidebar.classList.remove('collapsed', 'visible');
|
sidebar.classList.remove('collapsed', 'visible');
|
||||||
sidebar.classList.add('auto-hide');
|
sidebar.classList.add('auto-hide');
|
||||||
hoverArea.classList.remove('disabled');
|
hoverArea.classList.remove('disabled');
|
||||||
|
|
||||||
if (this.isHovering) {
|
if (this.isHovering) {
|
||||||
sidebar.classList.add('hover-active');
|
sidebar.classList.add('hover-active');
|
||||||
this.isVisible = true;
|
this.isVisible = true;
|
||||||
@@ -739,7 +739,7 @@ export class SidebarManager {
|
|||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update container margin when sidebar state changes
|
// Update container margin when sidebar state changes
|
||||||
this.updateContainerMargin();
|
this.updateContainerMargin();
|
||||||
}
|
}
|
||||||
@@ -750,16 +750,16 @@ export class SidebarManager {
|
|||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
|
|
||||||
if (!container || !sidebar || this.isDisabledBySetting) return;
|
if (!container || !sidebar || this.isDisabledBySetting) return;
|
||||||
|
|
||||||
// Reset margin to default
|
// Reset margin to default
|
||||||
container.style.marginLeft = '';
|
container.style.marginLeft = '';
|
||||||
|
|
||||||
// Only adjust margin if sidebar is visible and pinned
|
// Only adjust margin if sidebar is visible and pinned
|
||||||
if ((this.isPinned || this.isHovering) && this.isVisible) {
|
if ((this.isPinned || this.isHovering) && this.isVisible) {
|
||||||
const sidebarWidth = sidebar.offsetWidth;
|
const sidebarWidth = sidebar.offsetWidth;
|
||||||
const viewportWidth = window.innerWidth;
|
const viewportWidth = window.innerWidth;
|
||||||
const containerWidth = container.offsetWidth;
|
const containerWidth = container.offsetWidth;
|
||||||
|
|
||||||
// Check if there's enough space for both sidebar and container
|
// Check if there's enough space for both sidebar and container
|
||||||
// We need: sidebar width + container width + some padding < viewport width
|
// We need: sidebar width + container width + some padding < viewport width
|
||||||
if (sidebarWidth + containerWidth + sidebarWidth > viewportWidth) {
|
if (sidebarWidth + containerWidth + sidebarWidth > viewportWidth) {
|
||||||
@@ -837,8 +837,8 @@ export class SidebarManager {
|
|||||||
const pinBtn = document.getElementById('sidebarPinToggle');
|
const pinBtn = document.getElementById('sidebarPinToggle');
|
||||||
if (pinBtn) {
|
if (pinBtn) {
|
||||||
pinBtn.classList.toggle('active', this.isPinned);
|
pinBtn.classList.toggle('active', this.isPinned);
|
||||||
pinBtn.title = this.isPinned
|
pinBtn.title = this.isPinned
|
||||||
? translate('sidebar.unpinSidebar')
|
? translate('sidebar.unpinSidebar')
|
||||||
: translate('sidebar.pinSidebar');
|
: translate('sidebar.pinSidebar');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -883,13 +883,13 @@ export class SidebarManager {
|
|||||||
renderTreeNode(nodeData, basePath) {
|
renderTreeNode(nodeData, basePath) {
|
||||||
const entries = Object.entries(nodeData);
|
const entries = Object.entries(nodeData);
|
||||||
if (entries.length === 0) return '';
|
if (entries.length === 0) return '';
|
||||||
|
|
||||||
return entries.map(([folderName, children]) => {
|
return entries.map(([folderName, children]) => {
|
||||||
const currentPath = basePath ? `${basePath}/${folderName}` : folderName;
|
const currentPath = basePath ? `${basePath}/${folderName}` : folderName;
|
||||||
const hasChildren = Object.keys(children).length > 0;
|
const hasChildren = Object.keys(children).length > 0;
|
||||||
const isExpanded = this.expandedNodes.has(currentPath);
|
const isExpanded = this.expandedNodes.has(currentPath);
|
||||||
const isSelected = this.selectedPath === currentPath;
|
const isSelected = this.selectedPath === currentPath;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="sidebar-tree-node" data-path="${currentPath}">
|
<div class="sidebar-tree-node" data-path="${currentPath}">
|
||||||
<div class="sidebar-tree-node-content ${isSelected ? 'selected' : ''}" data-path="${currentPath}">
|
<div class="sidebar-tree-node-content ${isSelected ? 'selected' : ''}" data-path="${currentPath}">
|
||||||
@@ -934,7 +934,7 @@ export class SidebarManager {
|
|||||||
const foldersHtml = this.foldersList.map(folder => {
|
const foldersHtml = this.foldersList.map(folder => {
|
||||||
const displayName = folder === '' ? '/' : folder;
|
const displayName = folder === '' ? '/' : folder;
|
||||||
const isSelected = this.selectedPath === folder;
|
const isSelected = this.selectedPath === folder;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="sidebar-folder-item ${isSelected ? 'selected' : ''}" data-path="${folder}">
|
<div class="sidebar-folder-item ${isSelected ? 'selected' : ''}" data-path="${folder}">
|
||||||
<div class="sidebar-node-content" data-path="${folder}">
|
<div class="sidebar-node-content" data-path="${folder}">
|
||||||
@@ -956,13 +956,13 @@ export class SidebarManager {
|
|||||||
|
|
||||||
const expandIcon = event.target.closest('.sidebar-tree-expand-icon');
|
const expandIcon = event.target.closest('.sidebar-tree-expand-icon');
|
||||||
const nodeContent = event.target.closest('.sidebar-tree-node-content');
|
const nodeContent = event.target.closest('.sidebar-tree-node-content');
|
||||||
|
|
||||||
if (expandIcon) {
|
if (expandIcon) {
|
||||||
// Toggle expand/collapse
|
// Toggle expand/collapse
|
||||||
const treeNode = expandIcon.closest('.sidebar-tree-node');
|
const treeNode = expandIcon.closest('.sidebar-tree-node');
|
||||||
const path = treeNode.dataset.path;
|
const path = treeNode.dataset.path;
|
||||||
const children = treeNode.querySelector('.sidebar-tree-children');
|
const children = treeNode.querySelector('.sidebar-tree-children');
|
||||||
|
|
||||||
if (this.expandedNodes.has(path)) {
|
if (this.expandedNodes.has(path)) {
|
||||||
this.expandedNodes.delete(path);
|
this.expandedNodes.delete(path);
|
||||||
expandIcon.classList.remove('expanded');
|
expandIcon.classList.remove('expanded');
|
||||||
@@ -972,7 +972,7 @@ export class SidebarManager {
|
|||||||
expandIcon.classList.add('expanded');
|
expandIcon.classList.add('expanded');
|
||||||
if (children) children.classList.add('expanded');
|
if (children) children.classList.add('expanded');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveExpandedState();
|
this.saveExpandedState();
|
||||||
} else if (nodeContent) {
|
} else if (nodeContent) {
|
||||||
// Select folder
|
// Select folder
|
||||||
@@ -985,7 +985,7 @@ export class SidebarManager {
|
|||||||
handleBreadcrumbClick(event) {
|
handleBreadcrumbClick(event) {
|
||||||
const breadcrumbItem = event.target.closest('.sidebar-breadcrumb-item');
|
const breadcrumbItem = event.target.closest('.sidebar-breadcrumb-item');
|
||||||
const dropdownItem = event.target.closest('.breadcrumb-dropdown-item');
|
const dropdownItem = event.target.closest('.breadcrumb-dropdown-item');
|
||||||
|
|
||||||
if (dropdownItem) {
|
if (dropdownItem) {
|
||||||
// Handle dropdown item selection
|
// Handle dropdown item selection
|
||||||
const path = dropdownItem.dataset.path || '';
|
const path = dropdownItem.dataset.path || '';
|
||||||
@@ -997,17 +997,17 @@ export class SidebarManager {
|
|||||||
const isPlaceholder = breadcrumbItem.classList.contains('placeholder');
|
const isPlaceholder = breadcrumbItem.classList.contains('placeholder');
|
||||||
const isActive = breadcrumbItem.classList.contains('active');
|
const isActive = breadcrumbItem.classList.contains('active');
|
||||||
const dropdown = breadcrumbItem.closest('.breadcrumb-dropdown');
|
const dropdown = breadcrumbItem.closest('.breadcrumb-dropdown');
|
||||||
|
|
||||||
if (isPlaceholder || (isActive && path === this.selectedPath)) {
|
if (isPlaceholder || (isActive && path === this.selectedPath)) {
|
||||||
// Open dropdown for placeholders or active items
|
// Open dropdown for placeholders or active items
|
||||||
// Close any open dropdown first
|
// Close any open dropdown first
|
||||||
if (this.openDropdown && this.openDropdown !== dropdown) {
|
if (this.openDropdown && this.openDropdown !== dropdown) {
|
||||||
this.openDropdown.classList.remove('open');
|
this.openDropdown.classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle current dropdown
|
// Toggle current dropdown
|
||||||
dropdown.classList.toggle('open');
|
dropdown.classList.toggle('open');
|
||||||
|
|
||||||
// Update open dropdown reference
|
// Update open dropdown reference
|
||||||
this.openDropdown = dropdown.classList.contains('open') ? dropdown : null;
|
this.openDropdown = dropdown.classList.contains('open') ? dropdown : null;
|
||||||
} else {
|
} else {
|
||||||
@@ -1025,21 +1025,24 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async selectFolder(path) {
|
async selectFolder(path) {
|
||||||
|
// Normalize path: null or undefined means root
|
||||||
|
const normalizedPath = (path === null || path === undefined) ? '' : path;
|
||||||
|
|
||||||
// Update selected path
|
// Update selected path
|
||||||
this.selectedPath = path;
|
this.selectedPath = normalizedPath;
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
this.updateTreeSelection();
|
this.updateTreeSelection();
|
||||||
this.updateBreadcrumbs();
|
this.updateBreadcrumbs();
|
||||||
this.updateSidebarHeader();
|
this.updateSidebarHeader();
|
||||||
|
|
||||||
// Update page state
|
// Update page state
|
||||||
this.pageControls.pageState.activeFolder = path;
|
this.pageControls.pageState.activeFolder = normalizedPath;
|
||||||
setStorageItem(`${this.pageType}_activeFolder`, path);
|
setStorageItem(`${this.pageType}_activeFolder`, normalizedPath);
|
||||||
|
|
||||||
// Reload models with new filter
|
// Reload models with new filter
|
||||||
await this.pageControls.resetAndReload();
|
await this.pageControls.resetAndReload();
|
||||||
|
|
||||||
// Auto-hide sidebar on mobile after selection
|
// Auto-hide sidebar on mobile after selection
|
||||||
if (window.innerWidth <= 1024) {
|
if (window.innerWidth <= 1024) {
|
||||||
this.hideSidebar();
|
this.hideSidebar();
|
||||||
@@ -1048,7 +1051,7 @@ export class SidebarManager {
|
|||||||
|
|
||||||
handleFolderListClick(event) {
|
handleFolderListClick(event) {
|
||||||
const folderItem = event.target.closest('.sidebar-folder-item');
|
const folderItem = event.target.closest('.sidebar-folder-item');
|
||||||
|
|
||||||
if (folderItem) {
|
if (folderItem) {
|
||||||
const path = folderItem.dataset.path;
|
const path = folderItem.dataset.path;
|
||||||
this.selectFolder(path);
|
this.selectFolder(path);
|
||||||
@@ -1150,15 +1153,15 @@ export class SidebarManager {
|
|||||||
updateTreeSelection() {
|
updateTreeSelection() {
|
||||||
const folderTree = document.getElementById('sidebarFolderTree');
|
const folderTree = document.getElementById('sidebarFolderTree');
|
||||||
if (!folderTree) return;
|
if (!folderTree) return;
|
||||||
|
|
||||||
if (this.displayMode === 'list') {
|
if (this.displayMode === 'list') {
|
||||||
// Remove all selections in list mode
|
// Remove all selections in list mode
|
||||||
folderTree.querySelectorAll('.sidebar-folder-item').forEach(item => {
|
folderTree.querySelectorAll('.sidebar-folder-item').forEach(item => {
|
||||||
item.classList.remove('selected');
|
item.classList.remove('selected');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add selection to current path
|
// Add selection to current path
|
||||||
if (this.selectedPath !== null) {
|
if (this.selectedPath !== null && this.selectedPath !== undefined) {
|
||||||
const selectedItem = folderTree.querySelector(`[data-path="${this.selectedPath}"]`);
|
const selectedItem = folderTree.querySelector(`[data-path="${this.selectedPath}"]`);
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
selectedItem.classList.add('selected');
|
selectedItem.classList.add('selected');
|
||||||
@@ -1168,8 +1171,8 @@ export class SidebarManager {
|
|||||||
folderTree.querySelectorAll('.sidebar-tree-node-content').forEach(node => {
|
folderTree.querySelectorAll('.sidebar-tree-node-content').forEach(node => {
|
||||||
node.classList.remove('selected');
|
node.classList.remove('selected');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.selectedPath) {
|
if (this.selectedPath !== null && this.selectedPath !== undefined) {
|
||||||
const selectedNode = folderTree.querySelector(`[data-path="${this.selectedPath}"] .sidebar-tree-node-content`);
|
const selectedNode = folderTree.querySelector(`[data-path="${this.selectedPath}"] .sidebar-tree-node-content`);
|
||||||
if (selectedNode) {
|
if (selectedNode) {
|
||||||
selectedNode.classList.add('selected');
|
selectedNode.classList.add('selected');
|
||||||
@@ -1181,15 +1184,15 @@ export class SidebarManager {
|
|||||||
|
|
||||||
expandPathParents(path) {
|
expandPathParents(path) {
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
|
|
||||||
const parts = path.split('/');
|
const parts = path.split('/');
|
||||||
let currentPath = '';
|
let currentPath = '';
|
||||||
|
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
|
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
|
||||||
this.expandedNodes.add(currentPath);
|
this.expandedNodes.add(currentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderTree();
|
this.renderTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1199,7 +1202,7 @@ export class SidebarManager {
|
|||||||
// Root level siblings are top-level folders
|
// Root level siblings are top-level folders
|
||||||
return Object.keys(this.treeData);
|
return Object.keys(this.treeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to the parent folder to get siblings
|
// Navigate to the parent folder to get siblings
|
||||||
let currentNode = this.treeData;
|
let currentNode = this.treeData;
|
||||||
for (let i = 0; i < level; i++) {
|
for (let i = 0; i < level; i++) {
|
||||||
@@ -1208,7 +1211,7 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
currentNode = currentNode[pathParts[i]];
|
currentNode = currentNode[pathParts[i]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(currentNode);
|
return Object.keys(currentNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1217,37 +1220,38 @@ export class SidebarManager {
|
|||||||
if (!path) {
|
if (!path) {
|
||||||
return Object.keys(this.treeData);
|
return Object.keys(this.treeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = path.split('/');
|
const parts = path.split('/');
|
||||||
let currentNode = this.treeData;
|
let currentNode = this.treeData;
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (!currentNode[part]) {
|
if (!currentNode[part]) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
currentNode = currentNode[part];
|
currentNode = currentNode[part];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(currentNode);
|
return Object.keys(currentNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBreadcrumbs() {
|
updateBreadcrumbs() {
|
||||||
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
|
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
|
||||||
if (!sidebarBreadcrumbNav) return;
|
if (!sidebarBreadcrumbNav) return;
|
||||||
|
|
||||||
const parts = this.selectedPath ? this.selectedPath.split('/') : [];
|
const parts = this.selectedPath ? this.selectedPath.split('/') : [];
|
||||||
let currentPath = '';
|
let currentPath = '';
|
||||||
|
|
||||||
// Start with root breadcrumb
|
// Start with root breadcrumb
|
||||||
const rootSiblings = Object.keys(this.treeData);
|
const rootSiblings = Object.keys(this.treeData);
|
||||||
|
const isRootSelected = !this.selectedPath;
|
||||||
const breadcrumbs = [`
|
const breadcrumbs = [`
|
||||||
<div class="breadcrumb-dropdown">
|
<div class="breadcrumb-dropdown">
|
||||||
<span class="sidebar-breadcrumb-item ${this.selectedPath == null ? 'active' : ''}" data-path="">
|
<span class="sidebar-breadcrumb-item ${isRootSelected ? 'active' : ''}" data-path="">
|
||||||
<i class="fas fa-home"></i> ${this.apiClient.apiConfig.config.displayName} root
|
<i class="fas fa-home"></i> ${this.apiClient.apiConfig.config.displayName} root
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`];
|
`];
|
||||||
|
|
||||||
// Add separator and placeholder for next level if we're at root
|
// Add separator and placeholder for next level if we're at root
|
||||||
if (!this.selectedPath) {
|
if (!this.selectedPath) {
|
||||||
const nextLevelFolders = rootSiblings;
|
const nextLevelFolders = rootSiblings;
|
||||||
@@ -1266,21 +1270,21 @@ export class SidebarManager {
|
|||||||
<div class="breadcrumb-dropdown-item" data-path="${folder}">
|
<div class="breadcrumb-dropdown-item" data-path="${folder}">
|
||||||
${folder}
|
${folder}
|
||||||
</div>`).join('')
|
</div>`).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add breadcrumb items for each path segment
|
// Add breadcrumb items for each path segment
|
||||||
parts.forEach((part, index) => {
|
parts.forEach((part, index) => {
|
||||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||||
const isLast = index === parts.length - 1;
|
const isLast = index === parts.length - 1;
|
||||||
|
|
||||||
// Get siblings for this level
|
// Get siblings for this level
|
||||||
const siblings = this.getSiblingFolders(parts, index);
|
const siblings = this.getSiblingFolders(parts, index);
|
||||||
|
|
||||||
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
|
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
|
||||||
breadcrumbs.push(`
|
breadcrumbs.push(`
|
||||||
<div class="breadcrumb-dropdown">
|
<div class="breadcrumb-dropdown">
|
||||||
@@ -1299,12 +1303,12 @@ export class SidebarManager {
|
|||||||
data-path="${currentPath.replace(part, folder)}">
|
data-path="${currentPath.replace(part, folder)}">
|
||||||
${folder}
|
${folder}
|
||||||
</div>`).join('')
|
</div>`).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Add separator and placeholder for next level if not the last item
|
// Add separator and placeholder for next level if not the last item
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
const childFolders = this.getChildFolders(currentPath);
|
const childFolders = this.getChildFolders(currentPath);
|
||||||
@@ -1323,22 +1327,22 @@ export class SidebarManager {
|
|||||||
<div class="breadcrumb-dropdown-item" data-path="${currentPath}/${folder}">
|
<div class="breadcrumb-dropdown-item" data-path="${currentPath}/${folder}">
|
||||||
${folder}
|
${folder}
|
||||||
</div>`).join('')
|
</div>`).join('')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sidebarBreadcrumbNav.innerHTML = breadcrumbs.join('');
|
sidebarBreadcrumbNav.innerHTML = breadcrumbs.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSidebarHeader() {
|
updateSidebarHeader() {
|
||||||
const sidebarHeader = document.getElementById('sidebarHeader');
|
const sidebarHeader = document.getElementById('sidebarHeader');
|
||||||
if (!sidebarHeader) return;
|
if (!sidebarHeader) return;
|
||||||
|
|
||||||
if (this.selectedPath == null) {
|
if (!this.selectedPath) {
|
||||||
sidebarHeader.classList.add('root-selected');
|
sidebarHeader.classList.add('root-selected');
|
||||||
} else {
|
} else {
|
||||||
sidebarHeader.classList.remove('root-selected');
|
sidebarHeader.classList.remove('root-selected');
|
||||||
@@ -1348,11 +1352,11 @@ export class SidebarManager {
|
|||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
|
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
|
||||||
|
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
|
|
||||||
this.isVisible = !this.isVisible;
|
this.isVisible = !this.isVisible;
|
||||||
|
|
||||||
if (this.isVisible) {
|
if (this.isVisible) {
|
||||||
sidebar.classList.remove('collapsed');
|
sidebar.classList.remove('collapsed');
|
||||||
sidebar.classList.add('visible');
|
sidebar.classList.add('visible');
|
||||||
@@ -1360,28 +1364,28 @@ export class SidebarManager {
|
|||||||
sidebar.classList.remove('visible');
|
sidebar.classList.remove('visible');
|
||||||
sidebar.classList.add('collapsed');
|
sidebar.classList.add('collapsed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toggleBtn) {
|
if (toggleBtn) {
|
||||||
toggleBtn.classList.toggle('active', this.isVisible);
|
toggleBtn.classList.toggle('active', this.isVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveSidebarState();
|
this.saveSidebarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeSidebar() {
|
closeSidebar() {
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
|
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
|
||||||
|
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
|
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
sidebar.classList.remove('visible');
|
sidebar.classList.remove('visible');
|
||||||
sidebar.classList.add('collapsed');
|
sidebar.classList.add('collapsed');
|
||||||
|
|
||||||
if (toggleBtn) {
|
if (toggleBtn) {
|
||||||
toggleBtn.classList.remove('active');
|
toggleBtn.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveSidebarState();
|
this.saveSidebarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1390,12 +1394,12 @@ export class SidebarManager {
|
|||||||
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
|
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
|
||||||
const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree'
|
const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree'
|
||||||
const recursiveSearchEnabled = getStorageItem(`${this.pageType}_recursiveSearch`, true);
|
const recursiveSearchEnabled = getStorageItem(`${this.pageType}_recursiveSearch`, true);
|
||||||
|
|
||||||
this.isPinned = isPinned;
|
this.isPinned = isPinned;
|
||||||
this.expandedNodes = new Set(expandedPaths);
|
this.expandedNodes = new Set(expandedPaths);
|
||||||
this.displayMode = displayMode;
|
this.displayMode = displayMode;
|
||||||
this.recursiveSearchEnabled = recursiveSearchEnabled;
|
this.recursiveSearchEnabled = recursiveSearchEnabled;
|
||||||
|
|
||||||
this.updatePinButton();
|
this.updatePinButton();
|
||||||
this.updateDisplayModeButton();
|
this.updateDisplayModeButton();
|
||||||
this.updateCollapseAllButton();
|
this.updateCollapseAllButton();
|
||||||
|
|||||||
92
tests/services/test_root_folder_recursive.py
Normal file
92
tests/services/test_root_folder_recursive.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import pytest
|
||||||
|
from py.services.model_query import ModelFilterSet, FilterCriteria
|
||||||
|
from py.services.recipe_scanner import RecipeScanner
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
# Mock settings
|
||||||
|
class MockSettings:
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return default
|
||||||
|
|
||||||
|
# --- Model Filtering Tests ---
|
||||||
|
|
||||||
|
def test_model_filter_set_root_recursive_true():
|
||||||
|
filter_set = ModelFilterSet(MockSettings())
|
||||||
|
items = [
|
||||||
|
{"model_name": "root_item", "folder": ""},
|
||||||
|
{"model_name": "sub_item", "folder": "sub"},
|
||||||
|
]
|
||||||
|
criteria = FilterCriteria(folder="", search_options={"recursive": True})
|
||||||
|
|
||||||
|
result = filter_set.apply(items, criteria)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
assert any(i["model_name"] == "root_item" for i in result)
|
||||||
|
assert any(i["model_name"] == "sub_item" for i in result)
|
||||||
|
|
||||||
|
def test_model_filter_set_root_recursive_false():
|
||||||
|
filter_set = ModelFilterSet(MockSettings())
|
||||||
|
items = [
|
||||||
|
{"model_name": "root_item", "folder": ""},
|
||||||
|
{"model_name": "sub_item", "folder": "sub"},
|
||||||
|
]
|
||||||
|
criteria = FilterCriteria(folder="", search_options={"recursive": False})
|
||||||
|
|
||||||
|
result = filter_set.apply(items, criteria)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["model_name"] == "root_item"
|
||||||
|
|
||||||
|
# --- Recipe Filtering Tests ---
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recipe_scanner_root_recursive_true():
|
||||||
|
# Mock LoraScanner
|
||||||
|
class StubLoraScanner:
|
||||||
|
async def get_cached_data(self):
|
||||||
|
return SimpleNamespace(raw_data=[])
|
||||||
|
|
||||||
|
scanner = RecipeScanner(lora_scanner=StubLoraScanner())
|
||||||
|
# Manually populate cache for testing get_paginated_data logic
|
||||||
|
scanner._cache = SimpleNamespace(
|
||||||
|
raw_data=[
|
||||||
|
{"id": "r1", "title": "root_recipe", "folder": "", "modified": 1.0, "created_date": 1.0, "loras": []},
|
||||||
|
{"id": "r2", "title": "sub_recipe", "folder": "sub", "modified": 2.0, "created_date": 2.0, "loras": []},
|
||||||
|
],
|
||||||
|
sorted_by_date=[
|
||||||
|
{"id": "r2", "title": "sub_recipe", "folder": "sub", "modified": 2.0, "created_date": 2.0, "loras": []},
|
||||||
|
{"id": "r1", "title": "root_recipe", "folder": "", "modified": 1.0, "created_date": 1.0, "loras": []},
|
||||||
|
],
|
||||||
|
sorted_by_name=[],
|
||||||
|
version_index={}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await scanner.get_paginated_data(page=1, page_size=10, folder="", recursive=True)
|
||||||
|
|
||||||
|
assert len(result["items"]) == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recipe_scanner_root_recursive_false():
|
||||||
|
# Mock LoraScanner
|
||||||
|
class StubLoraScanner:
|
||||||
|
async def get_cached_data(self):
|
||||||
|
return SimpleNamespace(raw_data=[])
|
||||||
|
|
||||||
|
scanner = RecipeScanner(lora_scanner=StubLoraScanner())
|
||||||
|
scanner._cache = SimpleNamespace(
|
||||||
|
raw_data=[
|
||||||
|
{"id": "r1", "title": "root_recipe", "folder": "", "modified": 1.0, "created_date": 1.0, "loras": []},
|
||||||
|
{"id": "r2", "title": "sub_recipe", "folder": "sub", "modified": 2.0, "created_date": 2.0, "loras": []},
|
||||||
|
],
|
||||||
|
sorted_by_date=[
|
||||||
|
{"id": "r2", "title": "sub_recipe", "folder": "sub", "modified": 2.0, "created_date": 2.0, "loras": []},
|
||||||
|
{"id": "r1", "title": "root_recipe", "folder": "", "modified": 1.0, "created_date": 1.0, "loras": []},
|
||||||
|
],
|
||||||
|
sorted_by_name=[],
|
||||||
|
version_index={}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await scanner.get_paginated_data(page=1, page_size=10, folder="", recursive=False)
|
||||||
|
|
||||||
|
assert len(result["items"]) == 1
|
||||||
|
assert result["items"][0]["id"] == "r1"
|
||||||
Reference in New Issue
Block a user