mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 06:32:12 -03:00
feat: implement tag filtering with include/exclude states
- Update frontend tag filter to cycle through include/exclude/clear states - Add backend support for tag_include and tag_exclude query parameters - Maintain backward compatibility with legacy tag parameter - Store tag states as dictionary with 'include'/'exclude' values - Update test matrix documentation to reflect new tag behavior The changes enable more granular tag filtering where users can now explicitly include or exclude specific tags, rather than just adding tags to a simple inclusion list. This provides better control over search results and improves the filtering user experience.
This commit is contained in:
@@ -806,9 +806,13 @@ export class BaseModelApiClient {
|
||||
params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false');
|
||||
|
||||
if (pageState.filters) {
|
||||
if (pageState.filters.tags && pageState.filters.tags.length > 0) {
|
||||
pageState.filters.tags.forEach(tag => {
|
||||
params.append('tag', tag);
|
||||
if (pageState.filters.tags && Object.keys(pageState.filters.tags).length > 0) {
|
||||
Object.entries(pageState.filters.tags).forEach(([tag, state]) => {
|
||||
if (state === 'include') {
|
||||
params.append('tag_include', tag);
|
||||
} else if (state === 'exclude') {
|
||||
params.append('tag_exclude', tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -66,8 +66,14 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
||||
}
|
||||
|
||||
// Add tag filters
|
||||
if (pageState.filters?.tags && pageState.filters.tags.length) {
|
||||
params.append('tags', pageState.filters.tags.join(','));
|
||||
if (pageState.filters?.tags && Object.keys(pageState.filters.tags).length) {
|
||||
Object.entries(pageState.filters.tags).forEach(([tag, state]) => {
|
||||
if (state === 'include') {
|
||||
params.append('tag_include', tag);
|
||||
} else if (state === 'exclude') {
|
||||
params.append('tag_exclude', tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,7 @@ export class FilterManager {
|
||||
this.currentPage = options.page || document.body.dataset.page || 'loras';
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
this.filters = pageState.filters || {
|
||||
baseModel: [],
|
||||
tags: [],
|
||||
license: {}
|
||||
};
|
||||
this.filters = this.initializeFilters(pageState ? pageState.filters : undefined);
|
||||
|
||||
this.filterPanel = document.getElementById('filterPanel');
|
||||
this.filterButton = document.getElementById('filterButton');
|
||||
@@ -28,6 +24,7 @@ export class FilterManager {
|
||||
// Store this instance in the state
|
||||
if (pageState) {
|
||||
pageState.filterManager = this;
|
||||
pageState.filters = this.cloneFilters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,17 +108,12 @@ export class FilterManager {
|
||||
tagEl.dataset.tag = tagName;
|
||||
tagEl.innerHTML = `${tagName} <span class="tag-count">${tag.count}</span>`;
|
||||
|
||||
// Add click handler to toggle selection and automatically apply
|
||||
// Add click handler to cycle through tri-state filter and automatically apply
|
||||
tagEl.addEventListener('click', async () => {
|
||||
tagEl.classList.toggle('active');
|
||||
|
||||
if (tagEl.classList.contains('active')) {
|
||||
if (!this.filters.tags.includes(tagName)) {
|
||||
this.filters.tags.push(tagName);
|
||||
}
|
||||
} else {
|
||||
this.filters.tags = this.filters.tags.filter(t => t !== tagName);
|
||||
}
|
||||
const currentState = (this.filters.tags && this.filters.tags[tagName]) || 'none';
|
||||
const newState = this.getNextTriStateState(currentState);
|
||||
this.setTagFilterState(tagName, newState);
|
||||
this.applyTagElementState(tagEl, newState);
|
||||
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
@@ -129,6 +121,7 @@ export class FilterManager {
|
||||
await this.applyFilters(false);
|
||||
});
|
||||
|
||||
this.applyTagElementState(tagEl, (this.filters.tags && this.filters.tags[tagName]) || 'none');
|
||||
tagsContainer.appendChild(tagEl);
|
||||
});
|
||||
}
|
||||
@@ -310,11 +303,8 @@ export class FilterManager {
|
||||
const modelTags = document.querySelectorAll('.tag-filter');
|
||||
modelTags.forEach(tag => {
|
||||
const tagName = tag.dataset.tag;
|
||||
if (this.filters.tags.includes(tagName)) {
|
||||
tag.classList.add('active');
|
||||
} else {
|
||||
tag.classList.remove('active');
|
||||
}
|
||||
const state = (this.filters.tags && this.filters.tags[tagName]) || 'none';
|
||||
this.applyTagElementState(tag, state);
|
||||
});
|
||||
|
||||
// Update license tags
|
||||
@@ -322,9 +312,9 @@ export class FilterManager {
|
||||
}
|
||||
|
||||
updateActiveFiltersCount() {
|
||||
const totalActiveFilters = this.filters.baseModel.length +
|
||||
this.filters.tags.length +
|
||||
(this.filters.license ? Object.keys(this.filters.license).length : 0);
|
||||
const tagFilterCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
||||
const licenseFilterCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
||||
const totalActiveFilters = this.filters.baseModel.length + tagFilterCount + licenseFilterCount;
|
||||
|
||||
if (this.activeFiltersCount) {
|
||||
if (totalActiveFilters > 0) {
|
||||
@@ -341,10 +331,11 @@ export class FilterManager {
|
||||
const storageKey = `${this.currentPage}_filters`;
|
||||
|
||||
// Save filters to localStorage
|
||||
setStorageItem(storageKey, this.filters);
|
||||
const filtersSnapshot = this.cloneFilters();
|
||||
setStorageItem(storageKey, filtersSnapshot);
|
||||
|
||||
// Update state with current filters
|
||||
pageState.filters = { ...this.filters };
|
||||
pageState.filters = filtersSnapshot;
|
||||
|
||||
// Call the appropriate manager's load method based on page type
|
||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||
@@ -359,7 +350,7 @@ export class FilterManager {
|
||||
this.filterButton.classList.add('active');
|
||||
if (showToastNotification) {
|
||||
const baseModelCount = this.filters.baseModel.length;
|
||||
const tagsCount = this.filters.tags.length;
|
||||
const tagsCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
||||
|
||||
let message = '';
|
||||
if (baseModelCount > 0 && tagsCount > 0) {
|
||||
@@ -382,15 +373,16 @@ export class FilterManager {
|
||||
|
||||
async clearFilters() {
|
||||
// Clear all filters
|
||||
this.filters = {
|
||||
this.filters = this.initializeFilters({
|
||||
...this.filters,
|
||||
baseModel: [],
|
||||
tags: [],
|
||||
license: {} // Initialize with empty object instead of deleting
|
||||
};
|
||||
tags: {},
|
||||
license: {}
|
||||
});
|
||||
|
||||
// Update state
|
||||
const pageState = getCurrentPageState();
|
||||
pageState.filters = { ...this.filters };
|
||||
pageState.filters = this.cloneFilters();
|
||||
|
||||
// Update UI
|
||||
this.updateTagSelections();
|
||||
@@ -424,15 +416,11 @@ export class FilterManager {
|
||||
if (savedFilters) {
|
||||
try {
|
||||
// Ensure backward compatibility with older filter format
|
||||
this.filters = {
|
||||
baseModel: savedFilters.baseModel || [],
|
||||
tags: savedFilters.tags || [],
|
||||
license: savedFilters.license || {}
|
||||
};
|
||||
this.filters = this.initializeFilters(savedFilters);
|
||||
|
||||
// Update state with loaded filters
|
||||
const pageState = getCurrentPageState();
|
||||
pageState.filters = { ...this.filters };
|
||||
pageState.filters = this.cloneFilters();
|
||||
|
||||
this.updateTagSelections();
|
||||
this.updateActiveFiltersCount();
|
||||
@@ -447,8 +435,109 @@ export class FilterManager {
|
||||
}
|
||||
|
||||
hasActiveFilters() {
|
||||
return this.filters.baseModel.length > 0 ||
|
||||
this.filters.tags.length > 0 ||
|
||||
(this.filters.license && Object.keys(this.filters.license).length > 0);
|
||||
const tagCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
||||
const licenseCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
||||
return this.filters.baseModel.length > 0 || tagCount > 0 || licenseCount > 0;
|
||||
}
|
||||
|
||||
initializeFilters(existingFilters = {}) {
|
||||
const source = existingFilters || {};
|
||||
return {
|
||||
...source,
|
||||
baseModel: Array.isArray(source.baseModel) ? [...source.baseModel] : [],
|
||||
tags: this.normalizeTagFilters(source.tags),
|
||||
license: this.normalizeLicenseFilters(source.license)
|
||||
};
|
||||
}
|
||||
|
||||
normalizeTagFilters(tagFilters) {
|
||||
if (!tagFilters) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (Array.isArray(tagFilters)) {
|
||||
return tagFilters.reduce((acc, tag) => {
|
||||
if (typeof tag === 'string' && tag.trim().length > 0) {
|
||||
acc[tag] = 'include';
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (typeof tagFilters === 'object') {
|
||||
const normalized = {};
|
||||
Object.entries(tagFilters).forEach(([tag, state]) => {
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
const normalizedState = typeof state === 'string' ? state.toLowerCase() : '';
|
||||
if (normalizedState === 'include' || normalizedState === 'exclude') {
|
||||
normalized[tag] = normalizedState;
|
||||
}
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
normalizeLicenseFilters(licenseFilters) {
|
||||
if (!licenseFilters || typeof licenseFilters !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
Object.entries(licenseFilters).forEach(([key, state]) => {
|
||||
const normalizedState = typeof state === 'string' ? state.toLowerCase() : '';
|
||||
if (normalizedState === 'include' || normalizedState === 'exclude') {
|
||||
normalized[key] = normalizedState;
|
||||
}
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
cloneFilters() {
|
||||
return {
|
||||
...this.filters,
|
||||
baseModel: [...(this.filters.baseModel || [])],
|
||||
tags: { ...(this.filters.tags || {}) },
|
||||
license: { ...(this.filters.license || {}) }
|
||||
};
|
||||
}
|
||||
|
||||
getNextTriStateState(currentState) {
|
||||
switch (currentState) {
|
||||
case 'none':
|
||||
return 'include';
|
||||
case 'include':
|
||||
return 'exclude';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
setTagFilterState(tagName, state) {
|
||||
if (!this.filters.tags) {
|
||||
this.filters.tags = {};
|
||||
}
|
||||
|
||||
if (state === 'none') {
|
||||
delete this.filters.tags[tagName];
|
||||
} else {
|
||||
this.filters.tags[tagName] = state;
|
||||
}
|
||||
}
|
||||
|
||||
applyTagElementState(element, state) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.classList.remove('active', 'exclude');
|
||||
if (state === 'include') {
|
||||
element.classList.add('active');
|
||||
} else if (state === 'exclude') {
|
||||
element.classList.add('exclude');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,18 +66,19 @@ export const state = {
|
||||
activeFolder: getStorageItem(`${MODEL_TYPES.LORA}_activeFolder`),
|
||||
activeLetterFilter: null,
|
||||
previewVersions: loraPreviewVersions,
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.LORA}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
},
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.LORA}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
license: {}
|
||||
},
|
||||
bulkMode: false,
|
||||
selectedLoras: new Set(),
|
||||
loraMetadataCache: new Map(),
|
||||
@@ -91,18 +92,19 @@ export const state = {
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'date',
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
title: true,
|
||||
tags: true,
|
||||
loraName: true,
|
||||
loraModel: true
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: [],
|
||||
search: ''
|
||||
},
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
title: true,
|
||||
tags: true,
|
||||
loraName: true,
|
||||
loraModel: true
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
license: {},
|
||||
search: ''
|
||||
},
|
||||
pageSize: 20,
|
||||
showFavoritesOnly: false,
|
||||
duplicatesMode: false,
|
||||
@@ -117,17 +119,18 @@ export const state = {
|
||||
sortBy: 'name',
|
||||
activeFolder: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_activeFolder`),
|
||||
previewVersions: checkpointPreviewVersions,
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
creator: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
},
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
creator: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
license: {}
|
||||
},
|
||||
modelType: 'checkpoint', // 'checkpoint' or 'diffusion_model'
|
||||
bulkMode: false,
|
||||
selectedModels: new Set(),
|
||||
@@ -145,18 +148,19 @@ export const state = {
|
||||
activeFolder: getStorageItem(`${MODEL_TYPES.EMBEDDING}_activeFolder`),
|
||||
activeLetterFilter: null,
|
||||
previewVersions: embeddingPreviewVersions,
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.EMBEDDING}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
},
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
recursive: getStorageItem(`${MODEL_TYPES.EMBEDDING}_recursiveSearch`, true),
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
license: {}
|
||||
},
|
||||
bulkMode: false,
|
||||
selectedModels: new Set(),
|
||||
metadataCache: new Map(),
|
||||
|
||||
Reference in New Issue
Block a user