mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-10 03:22:12 -03:00
feat(recipes): add 'Has Workflow' filter to the recipes filter panel
This commit is contained in:
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "LoRA-Verfügbarkeit",
|
"loraAvailability": "LoRA-Verfügbarkeit",
|
||||||
"availabilityReady": "Einsatzbereit",
|
"availabilityReady": "Einsatzbereit",
|
||||||
"availabilityMissing": "Mit fehlenden LoRAs",
|
"availabilityMissing": "Mit fehlenden LoRAs",
|
||||||
"availabilityDeleted": "Mit gelöschten LoRAs"
|
"availabilityDeleted": "Mit gelöschten LoRAs",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "Theme wechseln",
|
"toggle": "Theme wechseln",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "Lora Availability",
|
"loraAvailability": "Lora Availability",
|
||||||
"availabilityReady": "Ready to use",
|
"availabilityReady": "Ready to use",
|
||||||
"availabilityMissing": "Has missing",
|
"availabilityMissing": "Has missing",
|
||||||
"availabilityDeleted": "Has deleted"
|
"availabilityDeleted": "Has deleted",
|
||||||
|
"workflow": "Workflow",
|
||||||
|
"hasWorkflow": "Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "Toggle theme",
|
"toggle": "Toggle theme",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "Disponibilidad de LoRAs",
|
"loraAvailability": "Disponibilidad de LoRAs",
|
||||||
"availabilityReady": "Listos para usar",
|
"availabilityReady": "Listos para usar",
|
||||||
"availabilityMissing": "Con LoRAs faltantes",
|
"availabilityMissing": "Con LoRAs faltantes",
|
||||||
"availabilityDeleted": "Con LoRAs eliminados"
|
"availabilityDeleted": "Con LoRAs eliminados",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "Cambiar tema",
|
"toggle": "Cambiar tema",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "Disponibilité des LoRAs",
|
"loraAvailability": "Disponibilité des LoRAs",
|
||||||
"availabilityReady": "Prêts à l'emploi",
|
"availabilityReady": "Prêts à l'emploi",
|
||||||
"availabilityMissing": "Avec LoRAs manquants",
|
"availabilityMissing": "Avec LoRAs manquants",
|
||||||
"availabilityDeleted": "Avec LoRAs supprimés"
|
"availabilityDeleted": "Avec LoRAs supprimés",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "Basculer le thème",
|
"toggle": "Basculer le thème",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "זמינות LoRA",
|
"loraAvailability": "זמינות LoRA",
|
||||||
"availabilityReady": "מוכנים לשימוש",
|
"availabilityReady": "מוכנים לשימוש",
|
||||||
"availabilityMissing": "עם LoRAs חסרים",
|
"availabilityMissing": "עם LoRAs חסרים",
|
||||||
"availabilityDeleted": "עם LoRAs שנמחקו"
|
"availabilityDeleted": "עם LoRAs שנמחקו",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "החלף ערכת נושא",
|
"toggle": "החלף ערכת נושא",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "LoRA の利用状況",
|
"loraAvailability": "LoRA の利用状況",
|
||||||
"availabilityReady": "使用可能",
|
"availabilityReady": "使用可能",
|
||||||
"availabilityMissing": "不足 LoRA あり",
|
"availabilityMissing": "不足 LoRA あり",
|
||||||
"availabilityDeleted": "削除済み LoRA あり"
|
"availabilityDeleted": "削除済み LoRA あり",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "テーマの切り替え",
|
"toggle": "テーマの切り替え",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "LoRA 가용성",
|
"loraAvailability": "LoRA 가용성",
|
||||||
"availabilityReady": "바로 사용 가능",
|
"availabilityReady": "바로 사용 가능",
|
||||||
"availabilityMissing": "누락된 LoRA 있음",
|
"availabilityMissing": "누락된 LoRA 있음",
|
||||||
"availabilityDeleted": "삭제된 LoRA 있음"
|
"availabilityDeleted": "삭제된 LoRA 있음",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "테마 토글",
|
"toggle": "테마 토글",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "Доступность LoRAs",
|
"loraAvailability": "Доступность LoRAs",
|
||||||
"availabilityReady": "Готовы к использованию",
|
"availabilityReady": "Готовы к использованию",
|
||||||
"availabilityMissing": "Есть отсутствующие",
|
"availabilityMissing": "Есть отсутствующие",
|
||||||
"availabilityDeleted": "Есть удалённые"
|
"availabilityDeleted": "Есть удалённые",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "Переключить тему",
|
"toggle": "Переключить тему",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "LoRA 可用性",
|
"loraAvailability": "LoRA 可用性",
|
||||||
"availabilityReady": "可直接使用",
|
"availabilityReady": "可直接使用",
|
||||||
"availabilityMissing": "包含缺失 LoRA",
|
"availabilityMissing": "包含缺失 LoRA",
|
||||||
"availabilityDeleted": "包含已删除 LoRA"
|
"availabilityDeleted": "包含已删除 LoRA",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "切换主题",
|
"toggle": "切换主题",
|
||||||
|
|||||||
+3
-1
@@ -297,7 +297,9 @@
|
|||||||
"loraAvailability": "LoRA 可用性",
|
"loraAvailability": "LoRA 可用性",
|
||||||
"availabilityReady": "可直接使用",
|
"availabilityReady": "可直接使用",
|
||||||
"availabilityMissing": "包含缺少的 LoRA",
|
"availabilityMissing": "包含缺少的 LoRA",
|
||||||
"availabilityDeleted": "包含已刪除的 LoRA"
|
"availabilityDeleted": "包含已刪除的 LoRA",
|
||||||
|
"workflow": "[TODO: Translate] Workflow",
|
||||||
|
"hasWorkflow": "[TODO: Translate] Has Workflow"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"toggle": "切換主題",
|
"toggle": "切換主題",
|
||||||
|
|||||||
@@ -308,6 +308,9 @@ class RecipeListingHandler:
|
|||||||
if lora_availability:
|
if lora_availability:
|
||||||
filters["lora_availability"] = lora_availability
|
filters["lora_availability"] = lora_availability
|
||||||
|
|
||||||
|
if request.query.get("has_workflow", "").lower() == "true":
|
||||||
|
filters["has_workflow"] = True
|
||||||
|
|
||||||
lora_hash = request.query.get("lora_hash")
|
lora_hash = request.query.get("lora_hash")
|
||||||
checkpoint_hash = request.query.get("checkpoint_hash")
|
checkpoint_hash = request.query.get("checkpoint_hash")
|
||||||
|
|
||||||
|
|||||||
@@ -3738,6 +3738,12 @@ class RecipeScanner:
|
|||||||
if self._compute_availability_statuses(item) & selected
|
if self._compute_availability_statuses(item) & selected
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Filter by embedded workflow presence
|
||||||
|
if filters.get("has_workflow"):
|
||||||
|
filtered_data = [
|
||||||
|
item for item in filtered_data if item.get("has_workflow")
|
||||||
|
]
|
||||||
|
|
||||||
# Apply sorting if not already handled by pre-sorted cache
|
# Apply sorting if not already handled by pre-sorted cache
|
||||||
if ":" in sort_by or sort_field in ("loras_count", "random", "opened"):
|
if ":" in sort_by or sort_field in ("loras_count", "random", "opened"):
|
||||||
field, order = (sort_by.split(":") + ["desc"])[:2]
|
field, order = (sort_by.split(":") + ["desc"])[:2]
|
||||||
|
|||||||
@@ -183,6 +183,11 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
|||||||
if (pageState.filters?.loraAvailability && pageState.filters.loraAvailability.length > 0) {
|
if (pageState.filters?.loraAvailability && pageState.filters.loraAvailability.length > 0) {
|
||||||
params.append('lora_availability', pageState.filters.loraAvailability.join(','));
|
params.append('lora_availability', pageState.filters.loraAvailability.join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add has-workflow filter
|
||||||
|
if (pageState.filters?.hasWorkflow) {
|
||||||
|
params.append('has_workflow', 'true');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch recipes
|
// Fetch recipes
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ export class FilterManager {
|
|||||||
this.initializeLoraAvailabilityFilters();
|
this.initializeLoraAvailabilityFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add click handler for the has-workflow chip (recipes page only)
|
||||||
|
if (this.shouldShowLoraAvailabilityFilter()) {
|
||||||
|
this.initializeHasWorkflowFilter();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize tag logic toggle
|
// Initialize tag logic toggle
|
||||||
this.initializeTagLogicToggle();
|
this.initializeTagLogicToggle();
|
||||||
|
|
||||||
@@ -466,6 +471,29 @@ export class FilterManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeHasWorkflowFilter() {
|
||||||
|
const hasWorkflowTags = document.querySelectorAll('.has-workflow-tag');
|
||||||
|
hasWorkflowTags.forEach(tag => {
|
||||||
|
tag.addEventListener('click', async () => {
|
||||||
|
this.filters.hasWorkflow = !this.filters.hasWorkflow;
|
||||||
|
tag.classList.toggle('active', this.filters.hasWorkflow);
|
||||||
|
|
||||||
|
this.updateActiveFiltersCount();
|
||||||
|
await this.applyFilters(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update selections based on stored filters
|
||||||
|
this.updateHasWorkflowSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHasWorkflowSelection() {
|
||||||
|
const hasWorkflowTags = document.querySelectorAll('.has-workflow-tag');
|
||||||
|
hasWorkflowTags.forEach(tag => {
|
||||||
|
tag.classList.toggle('active', Boolean(this.filters.hasWorkflow));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createBaseModelTags() {
|
createBaseModelTags() {
|
||||||
const baseModelTagsContainer = document.getElementById('baseModelTags');
|
const baseModelTagsContainer = document.getElementById('baseModelTags');
|
||||||
if (!baseModelTagsContainer) return;
|
if (!baseModelTagsContainer) return;
|
||||||
@@ -732,6 +760,7 @@ export class FilterManager {
|
|||||||
// Update LoRA availability tags if visible on this page
|
// Update LoRA availability tags if visible on this page
|
||||||
if (this.shouldShowLoraAvailabilityFilter()) {
|
if (this.shouldShowLoraAvailabilityFilter()) {
|
||||||
this.updateLoraAvailabilitySelections();
|
this.updateLoraAvailabilitySelections();
|
||||||
|
this.updateHasWorkflowSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoTagEls = document.querySelectorAll('.auto-tag-filter');
|
const autoTagEls = document.querySelectorAll('.auto-tag-filter');
|
||||||
@@ -763,7 +792,8 @@ export class FilterManager {
|
|||||||
const baseModelCount = this.filters.baseModel.filter(m => m !== EMPTY_WILDCARD_MARKER).length;
|
const baseModelCount = this.filters.baseModel.filter(m => m !== EMPTY_WILDCARD_MARKER).length;
|
||||||
// Active when at least one availability status is deselected
|
// Active when at least one availability status is deselected
|
||||||
const loraAvailabilityCount = this.filters.loraAvailability?.length ?? 0;
|
const loraAvailabilityCount = this.filters.loraAvailability?.length ?? 0;
|
||||||
const totalActiveFilters = baseModelCount + tagFilterCount + autoTagFilterCount + licenseFilterCount + modelTypeFilterCount + loraAvailabilityCount;
|
const hasWorkflowCount = this.filters.hasWorkflow ? 1 : 0;
|
||||||
|
const totalActiveFilters = baseModelCount + tagFilterCount + autoTagFilterCount + licenseFilterCount + modelTypeFilterCount + loraAvailabilityCount + hasWorkflowCount;
|
||||||
|
|
||||||
if (this.activeFiltersCount) {
|
if (this.activeFiltersCount) {
|
||||||
if (totalActiveFilters > 0) {
|
if (totalActiveFilters > 0) {
|
||||||
@@ -861,6 +891,7 @@ export class FilterManager {
|
|||||||
license: {},
|
license: {},
|
||||||
modelTypes: [],
|
modelTypes: [],
|
||||||
loraAvailability: [],
|
loraAvailability: [],
|
||||||
|
hasWorkflow: false,
|
||||||
tagLogic: 'any'
|
tagLogic: 'any'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -954,7 +985,8 @@ export class FilterManager {
|
|||||||
autoTagCount > 0 ||
|
autoTagCount > 0 ||
|
||||||
licenseCount > 0 ||
|
licenseCount > 0 ||
|
||||||
modelTypeCount > 0 ||
|
modelTypeCount > 0 ||
|
||||||
loraAvailabilityCount > 0
|
loraAvailabilityCount > 0 ||
|
||||||
|
Boolean(this.filters.hasWorkflow)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,6 +1000,7 @@ export class FilterManager {
|
|||||||
license: this.shouldShowLicenseFilters() ? this.normalizeLicenseFilters(source.license) : {},
|
license: this.shouldShowLicenseFilters() ? this.normalizeLicenseFilters(source.license) : {},
|
||||||
modelTypes: this.normalizeModelTypeFilters(source.modelTypes),
|
modelTypes: this.normalizeModelTypeFilters(source.modelTypes),
|
||||||
loraAvailability: this.normalizeLoraAvailabilityFilters(source.loraAvailability),
|
loraAvailability: this.normalizeLoraAvailabilityFilters(source.loraAvailability),
|
||||||
|
hasWorkflow: Boolean(source.hasWorkflow),
|
||||||
tagLogic: source.tagLogic || 'any'
|
tagLogic: source.tagLogic || 'any'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1081,6 +1114,7 @@ export class FilterManager {
|
|||||||
license: { ...(this.filters.license || {}) },
|
license: { ...(this.filters.license || {}) },
|
||||||
modelTypes: [...(this.filters.modelTypes || [])],
|
modelTypes: [...(this.filters.modelTypes || [])],
|
||||||
loraAvailability: [...(this.filters.loraAvailability || [])],
|
loraAvailability: [...(this.filters.loraAvailability || [])],
|
||||||
|
hasWorkflow: Boolean(this.filters.hasWorkflow),
|
||||||
tagLogic: this.filters.tagLogic || 'any',
|
tagLogic: this.filters.tagLogic || 'any',
|
||||||
search: pageState?.filters?.search ?? ''
|
search: pageState?.filters?.search ?? ''
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -287,6 +287,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-section">
|
||||||
|
<h4>{{ t('header.filter.workflow') }}</h4>
|
||||||
|
<div class="filter-tags" id="hasWorkflowTags">
|
||||||
|
<div class="filter-tag has-workflow-tag">
|
||||||
|
{{ t('header.filter.hasWorkflow') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_page == 'loras' or current_page == 'checkpoints' or current_page == 'other' %}
|
{% if current_page == 'loras' or current_page == 'checkpoints' or current_page == 'other' %}
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('../../../static/js/state/index.js', () => ({
|
||||||
|
getCurrentPageState: vi.fn(() => ({
|
||||||
|
filters: {},
|
||||||
|
})),
|
||||||
|
state: {
|
||||||
|
currentPageType: 'recipes',
|
||||||
|
loadingManager: {
|
||||||
|
showSimpleLoading: vi.fn(),
|
||||||
|
hide: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||||
|
showToast: vi.fn(),
|
||||||
|
updatePanelPositions: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
||||||
|
getModelApiClient: vi.fn(() => ({
|
||||||
|
loadMoreWithVirtualScroll: vi.fn().mockResolvedValue(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/storageHelpers.js', () => ({
|
||||||
|
getStorageItem: vi.fn(),
|
||||||
|
setStorageItem: vi.fn(),
|
||||||
|
removeStorageItem: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
|
||||||
|
translate: vi.fn((key, _params, fallback) => fallback || key),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/managers/FilterPresetManager.js', () => ({
|
||||||
|
FilterPresetManager: vi.fn().mockImplementation(() => ({
|
||||||
|
renderPresets: vi.fn(),
|
||||||
|
saveActivePreset: vi.fn(),
|
||||||
|
restoreActivePreset: vi.fn(),
|
||||||
|
updateAddButtonState: vi.fn(),
|
||||||
|
hasEmptyWildcardResult: vi.fn(() => false),
|
||||||
|
})),
|
||||||
|
EMPTY_WILDCARD_MARKER: '__EMPTY_WILDCARD_RESULT__',
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { FilterManager } from '../../../static/js/managers/FilterManager.js';
|
||||||
|
import { getStorageItem } from '../../../static/js/utils/storageHelpers.js';
|
||||||
|
|
||||||
|
describe('FilterManager - Has Workflow', () => {
|
||||||
|
let manager;
|
||||||
|
let mockFilterPanel;
|
||||||
|
let mockActiveFiltersCount;
|
||||||
|
|
||||||
|
function createHasWorkflowTag() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'hasWorkflowTags';
|
||||||
|
const tag = document.createElement('div');
|
||||||
|
tag.className = 'filter-tag has-workflow-tag';
|
||||||
|
container.appendChild(tag);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
getStorageItem.mockReturnValue(undefined);
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
|
||||||
|
mockFilterPanel = document.createElement('div');
|
||||||
|
mockFilterPanel.id = 'filterPanel';
|
||||||
|
mockFilterPanel.classList.add('hidden');
|
||||||
|
document.body.appendChild(mockFilterPanel);
|
||||||
|
|
||||||
|
mockActiveFiltersCount = document.createElement('span');
|
||||||
|
createHasWorkflowTag();
|
||||||
|
|
||||||
|
const originalGetElementById = document.getElementById;
|
||||||
|
document.getElementById = vi.fn((id) => {
|
||||||
|
if (id === 'filterPanel') return mockFilterPanel;
|
||||||
|
if (id === 'filterButton') return document.createElement('button');
|
||||||
|
if (id === 'activeFiltersCount') return mockActiveFiltersCount;
|
||||||
|
if (id === 'baseModelTags') return document.createElement('div');
|
||||||
|
if (id === 'modelTypeTags') return document.createElement('div');
|
||||||
|
return originalGetElementById.call(document, id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeFilters', () => {
|
||||||
|
it('should default to false on the recipes page', () => {
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
expect(manager.filters.hasWorkflow).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore a saved true value from storage', () => {
|
||||||
|
getStorageItem.mockReturnValue({
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
hasWorkflow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
expect(manager.filters.hasWorkflow).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should coerce a truthy stored value to boolean', () => {
|
||||||
|
getStorageItem.mockReturnValue({
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
hasWorkflow: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
expect(manager.filters.hasWorkflow).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasActiveFilters', () => {
|
||||||
|
it('should be inactive when hasWorkflow is off', () => {
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
expect(manager.hasActiveFilters()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be active when hasWorkflow is on', () => {
|
||||||
|
getStorageItem.mockReturnValue({
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
hasWorkflow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
expect(manager.hasActiveFilters()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateActiveFiltersCount', () => {
|
||||||
|
it('should count hasWorkflow as one active filter', () => {
|
||||||
|
getStorageItem.mockReturnValue({
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
hasWorkflow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
expect(mockActiveFiltersCount.textContent).toBe('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('chip interaction', () => {
|
||||||
|
it('should activate the filter when the chip is clicked', async () => {
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
const tag = document.querySelector('.has-workflow-tag');
|
||||||
|
expect(tag.classList.contains('active')).toBe(false);
|
||||||
|
|
||||||
|
tag.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
expect(manager.filters.hasWorkflow).toBe(true);
|
||||||
|
expect(tag.classList.contains('active')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deactivate the filter when the chip is clicked again', async () => {
|
||||||
|
getStorageItem.mockReturnValue({
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
hasWorkflow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
const tag = document.querySelector('.has-workflow-tag');
|
||||||
|
// Restored state should mark the chip active
|
||||||
|
expect(tag.classList.contains('active')).toBe(true);
|
||||||
|
|
||||||
|
tag.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
expect(manager.filters.hasWorkflow).toBe(false);
|
||||||
|
expect(tag.classList.contains('active')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cloneFilters', () => {
|
||||||
|
it('should include hasWorkflow in cloned filters', () => {
|
||||||
|
getStorageItem.mockReturnValue({
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
hasWorkflow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
const cloned = manager.cloneFilters();
|
||||||
|
|
||||||
|
expect(cloned.hasWorkflow).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clone an unset hasWorkflow as false', () => {
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
|
||||||
|
const cloned = manager.cloneFilters();
|
||||||
|
|
||||||
|
expect(cloned.hasWorkflow).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearFilters', () => {
|
||||||
|
it('should reset hasWorkflow to false', () => {
|
||||||
|
getStorageItem.mockReturnValue({
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
hasWorkflow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new FilterManager({ page: 'recipes' });
|
||||||
|
expect(manager.filters.hasWorkflow).toBe(true);
|
||||||
|
|
||||||
|
manager.clearFilters();
|
||||||
|
|
||||||
|
expect(manager.filters.hasWorkflow).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -758,6 +758,31 @@ async def test_list_recipes_ignores_invalid_lora_availability_values(
|
|||||||
assert "lora_availability" not in filters
|
assert "lora_availability" not in filters
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_recipes_passes_has_workflow_filter(
|
||||||
|
monkeypatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
|
response = await harness.client.get("/api/lm/recipes?has_workflow=true")
|
||||||
|
payload = await response.json()
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["items"] == []
|
||||||
|
assert harness.scanner.last_paginated_params is not None
|
||||||
|
filters = harness.scanner.last_paginated_params["filters"]
|
||||||
|
assert filters["has_workflow"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_recipes_ignores_non_true_has_workflow(
|
||||||
|
monkeypatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
|
response = await harness.client.get("/api/lm/recipes?has_workflow=bogus")
|
||||||
|
assert response.status == 200
|
||||||
|
assert harness.scanner.last_paginated_params is not None
|
||||||
|
filters = harness.scanner.last_paginated_params["filters"]
|
||||||
|
assert "has_workflow" not in filters
|
||||||
|
|
||||||
|
|
||||||
async def test_get_recipes_for_checkpoint(monkeypatch, tmp_path: Path) -> None:
|
async def test_get_recipes_for_checkpoint(monkeypatch, tmp_path: Path) -> None:
|
||||||
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
harness.scanner.checkpoint_lookup["abc123"] = [
|
harness.scanner.checkpoint_lookup["abc123"] = [
|
||||||
|
|||||||
@@ -2064,6 +2064,52 @@ async def test_get_paginated_data_filters_by_favorite(recipe_scanner):
|
|||||||
assert len(result_fav_false["items"]) == 2
|
assert len(result_fav_false["items"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_paginated_data_filters_by_has_workflow(recipe_scanner):
|
||||||
|
scanner, _ = recipe_scanner
|
||||||
|
|
||||||
|
await scanner.add_recipe(
|
||||||
|
{
|
||||||
|
"id": "no-workflow",
|
||||||
|
"file_path": "path/no-workflow.png",
|
||||||
|
"title": "No Workflow Recipe",
|
||||||
|
"modified": 1.0,
|
||||||
|
"created_date": 1.0,
|
||||||
|
"loras": [],
|
||||||
|
"has_workflow": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await scanner.add_recipe(
|
||||||
|
{
|
||||||
|
"id": "with-workflow",
|
||||||
|
"file_path": "path/with-workflow.png",
|
||||||
|
"title": "Workflow Recipe",
|
||||||
|
"modified": 2.0,
|
||||||
|
"created_date": 2.0,
|
||||||
|
"loras": [],
|
||||||
|
"has_workflow": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
await _wait_for_resort(scanner)
|
||||||
|
|
||||||
|
result_all = await scanner.get_paginated_data(page=1, page_size=10)
|
||||||
|
assert len(result_all["items"]) == 2
|
||||||
|
|
||||||
|
result_wf = await scanner.get_paginated_data(
|
||||||
|
page=1, page_size=10, filters={"has_workflow": True}
|
||||||
|
)
|
||||||
|
assert len(result_wf["items"]) == 1
|
||||||
|
assert result_wf["items"][0]["id"] == "with-workflow"
|
||||||
|
|
||||||
|
result_wf_false = await scanner.get_paginated_data(
|
||||||
|
page=1, page_size=10, filters={"has_workflow": False}
|
||||||
|
)
|
||||||
|
assert len(result_wf_false["items"]) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_paginated_data_filters_by_base_model_unknown_bucket(recipe_scanner):
|
async def test_get_paginated_data_filters_by_base_model_unknown_bucket(recipe_scanner):
|
||||||
scanner, _ = recipe_scanner
|
scanner, _ = recipe_scanner
|
||||||
|
|||||||
Reference in New Issue
Block a user