feat(recipes): add sync changes dropdown menu for recipe refresh

- Add syncChanges() function to recipeApi.js for quick refresh without cache rebuild
- Implement dropdown menu UI in recipes page with quick refresh and full rebuild options
- Add initDropdowns() method to RecipeManager for dropdown interaction handling
- Update AGENTS.md with more precise instruction about running sync_translation_keys.py
- Integrate sync changes functionality as default refresh behavior
This commit is contained in:
Will Miao
2026-03-04 20:31:58 +08:00
parent f402505f97
commit acc625ead3
4 changed files with 101 additions and 7 deletions

View File

@@ -135,7 +135,7 @@ npm run test:coverage # Generate coverage report
- ALWAYS use English for comments (per copilot-instructions.md) - ALWAYS use English for comments (per copilot-instructions.md)
- Dual mode: ComfyUI plugin (folder_paths) vs standalone (settings.json) - Dual mode: ComfyUI plugin (folder_paths) vs standalone (settings.json)
- Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"` - Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"`
- Run `python scripts/sync_translation_keys.py` after UI string updates - Run `python scripts/sync_translation_keys.py` after adding UI strings to `locales/en.json`
- Symlinks require normalized paths - Symlinks require normalized paths
## Frontend UI Architecture ## Frontend UI Architecture

View File

@@ -259,6 +259,26 @@ export async function resetAndReload(updateFolders = false) {
}); });
} }
/**
* Sync changes - quick refresh without rebuilding cache (similar to models page)
*/
export async function syncChanges() {
try {
state.loadingManager.showSimpleLoading('Syncing changes...');
// Simply reload the recipes without rebuilding cache
await resetAndReload();
showToast('toast.recipes.syncComplete', {}, 'success');
} catch (error) {
console.error('Error syncing recipes:', error);
showToast('toast.recipes.syncFailed', { message: error.message }, 'error');
} finally {
state.loadingManager.hide();
state.loadingManager.restoreProgressBar();
}
}
/** /**
* Refreshes the recipe list by first rebuilding the cache and then loading recipes * Refreshes the recipe list by first rebuilding the cache and then loading recipes
*/ */

View File

@@ -7,7 +7,7 @@ import { getSessionItem, removeSessionItem } from './utils/storageHelpers.js';
import { RecipeContextMenu } from './components/ContextMenu/index.js'; import { RecipeContextMenu } from './components/ContextMenu/index.js';
import { DuplicatesManager } from './components/DuplicatesManager.js'; import { DuplicatesManager } from './components/DuplicatesManager.js';
import { refreshVirtualScroll } from './utils/infiniteScroll.js'; import { refreshVirtualScroll } from './utils/infiniteScroll.js';
import { refreshRecipes, RecipeSidebarApiClient } from './api/recipeApi.js'; import { refreshRecipes, syncChanges, RecipeSidebarApiClient } from './api/recipeApi.js';
import { sidebarManager } from './components/SidebarManager.js'; import { sidebarManager } from './components/SidebarManager.js';
class RecipePageControls { class RecipePageControls {
@@ -27,7 +27,7 @@ class RecipePageControls {
return; return;
} }
refreshVirtualScroll(); await syncChanges();
} }
getSidebarApiClient() { getSidebarApiClient() {
@@ -236,6 +236,70 @@ class RecipeManager {
refreshVirtualScroll(); refreshVirtualScroll();
}); });
} }
// Initialize dropdown functionality for refresh button
this.initDropdowns();
}
initDropdowns() {
// Handle dropdown toggles
const dropdownToggles = document.querySelectorAll('.dropdown-toggle');
dropdownToggles.forEach(toggle => {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const dropdownGroup = toggle.closest('.dropdown-group');
// Close all other open dropdowns first
document.querySelectorAll('.dropdown-group.active').forEach(group => {
if (group !== dropdownGroup) {
group.classList.remove('active');
}
});
dropdownGroup.classList.toggle('active');
});
});
// Handle quick refresh option (Sync Changes)
const quickRefreshOption = document.querySelector('[data-action="quick-refresh"]');
if (quickRefreshOption) {
quickRefreshOption.addEventListener('click', (e) => {
e.stopPropagation();
this.pageControls.refreshModels(false);
this.closeDropdowns();
});
}
// Handle full rebuild option (Rebuild Cache)
const fullRebuildOption = document.querySelector('[data-action="full-rebuild"]');
if (fullRebuildOption) {
fullRebuildOption.addEventListener('click', (e) => {
e.stopPropagation();
this.pageControls.refreshModels(true);
this.closeDropdowns();
});
}
// Handle main refresh button (default: sync changes)
const refreshBtn = document.querySelector('[data-action="refresh"]');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.pageControls.refreshModels(false);
});
}
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.dropdown-group')) {
this.closeDropdowns();
}
});
}
closeDropdowns() {
document.querySelectorAll('.dropdown-group.active').forEach(group => {
group.classList.remove('active');
});
} }
// This method is kept for compatibility but now uses virtual scrolling // This method is kept for compatibility but now uses virtual scrolling

View File

@@ -66,10 +66,20 @@
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div title="{{ t('recipes.controls.refresh.title') }}" class="control-group"> <div title="{{ t('recipes.controls.refresh.title') }}" class="control-group dropdown-group">
<button onclick="recipeManager.refreshRecipes()"><i class="fas fa-sync"></i> {{ <button data-action="refresh" class="dropdown-main"><i class="fas fa-sync"></i> <span>{{
t('common.actions.refresh') t('common.actions.refresh') }}</span></button>
}}</button> <button class="dropdown-toggle" aria-label="Show refresh options">
<i class="fas fa-caret-down"></i>
</button>
<div class="dropdown-menu">
<div class="dropdown-item" data-action="quick-refresh" title="{{ t('recipes.controls.refresh.quickTooltip', default='Sync changes - quick refresh without rebuilding cache') }}">
<i class="fas fa-bolt"></i> <span>{{ t('loras.controls.refresh.quick', default='Sync Changes') }}</span>
</div>
<div class="dropdown-item" data-action="full-rebuild" title="{{ t('recipes.controls.refresh.fullTooltip', default='Rebuild cache - full rescan of all recipe files') }}">
<i class="fas fa-tools"></i> <span>{{ t('loras.controls.refresh.full', default='Rebuild Cache') }}</span>
</div>
</div>
</div> </div>
<div title="{{ t('recipes.controls.import.title') }}" class="control-group"> <div title="{{ t('recipes.controls.import.title') }}" class="control-group">
<button onclick="importManager.showImportModal()"><i class="fas fa-file-import"></i> {{ <button onclick="importManager.showImportModal()"><i class="fas fa-file-import"></i> {{