feat: Add recursive root folder scanning with API and UI updates. fixes #737

This commit is contained in:
Will Miao
2025-12-25 21:07:52 +08:00
parent e5869648fb
commit 91cd88f1df
4 changed files with 187 additions and 91 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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();

View 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"