mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Fix reprint loop on tag filters
- Fix endless loop if a tag was selected - Tag selection is now saved, both 'selected' and 'excluded' (old state is lost though) - Streamlined reprinting even more by refactoring bogus drilldown
This commit is contained in:
@ -24,6 +24,7 @@ export const FILTER_STATES = {
|
|||||||
EXCLUDED: { key: 'EXCLUDED', class: 'excluded' },
|
EXCLUDED: { key: 'EXCLUDED', class: 'excluded' },
|
||||||
UNDEFINED: { key: 'UNDEFINED', class: 'undefined' },
|
UNDEFINED: { key: 'UNDEFINED', class: 'undefined' },
|
||||||
};
|
};
|
||||||
|
export const DEFAULT_FILTER_STATE = FILTER_STATES.UNDEFINED.key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Robust check if one state equals the other. It does not care whether it's the state key or the state value object.
|
* Robust check if one state equals the other. It does not care whether it's the state key or the state value object.
|
||||||
@ -203,7 +204,7 @@ export class FilterHelper {
|
|||||||
return this.filterDataByState(data, state, isFolder);
|
return this.filterDataByState(data, state, isFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
filterDataByState(data, state, filterFunc, { includeFolders } = {}) {
|
filterDataByState(data, state, filterFunc, { includeFolders = false } = {}) {
|
||||||
if (isFilterState(state, FILTER_STATES.SELECTED)) {
|
if (isFilterState(state, FILTER_STATES.SELECTED)) {
|
||||||
return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag'));
|
return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag'));
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
event_types,
|
event_types,
|
||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js';
|
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
|
||||||
|
|
||||||
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
||||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js';
|
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js';
|
||||||
@ -180,6 +180,7 @@ function isBogusFolderOpen() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to be called when a specific tag/folder is chosen to "drill down".
|
* Function to be called when a specific tag/folder is chosen to "drill down".
|
||||||
|
*
|
||||||
* @param {*} source The jQuery element clicked when choosing the folder
|
* @param {*} source The jQuery element clicked when choosing the folder
|
||||||
* @param {string} tagId The tag id that is behind the chosen folder
|
* @param {string} tagId The tag id that is behind the chosen folder
|
||||||
* @param {boolean} remove Whether the given tag should be removed (otherwise it is added/chosen)
|
* @param {boolean} remove Whether the given tag should be removed (otherwise it is added/chosen)
|
||||||
@ -197,12 +198,9 @@ function chooseBogusFolder(source, tagId, remove = false) {
|
|||||||
// Instead of manually updating the filter conditions, we just "click" on the filter tag
|
// Instead of manually updating the filter conditions, we just "click" on the filter tag
|
||||||
// We search inside which filter block we are located in and use that one
|
// We search inside which filter block we are located in and use that one
|
||||||
const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter');
|
const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter');
|
||||||
if (remove) {
|
const tagElement = $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`);
|
||||||
// Click twice to skip over the 'excluded' state
|
|
||||||
$(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click').trigger('click');
|
toggleTagThreeState(tagElement, { stateOverride: DEFAULT_FILTER_STATE, simulateClick: true });
|
||||||
} else {
|
|
||||||
$(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -603,8 +601,8 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
|
|||||||
tagElement.find('.tag_name').text('').attr('title', tag.name).addClass(tag.icon);
|
tagElement.find('.tag_name').text('').attr('title', tag.name).addClass(tag.icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag.excluded && isGeneralList) {
|
if (selectable && isGeneralList) {
|
||||||
toggleTagThreeState(tagElement, { stateOverride: FILTER_STATES.EXCLUDED });
|
toggleTagThreeState(tagElement, { stateOverride: tag.filterState ?? DEFAULT_FILTER_STATE });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectable) {
|
if (selectable) {
|
||||||
@ -629,34 +627,28 @@ function onTagFilterClick(listElement) {
|
|||||||
|
|
||||||
let state = toggleTagThreeState($(this));
|
let state = toggleTagThreeState($(this));
|
||||||
|
|
||||||
// Manual undefined check required for three-state boolean
|
|
||||||
if (existingTag) {
|
if (existingTag) {
|
||||||
existingTag.excluded = isFilterState(state, FILTER_STATES.EXCLUDED);
|
existingTag.filterState = state;
|
||||||
|
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update bogus folder if applicable
|
// We don't print anything manually, updating the filter will automatically trigger a redraw of all relevant stuff
|
||||||
if (isBogusFolder(existingTag)) {
|
|
||||||
// Update bogus drilldown
|
|
||||||
if ($(this).hasClass('selected')) {
|
|
||||||
appendTagToList($('.rm_tag_controls .rm_tag_bogus_drilldown'), existingTag, { removable: true });
|
|
||||||
} else {
|
|
||||||
$(listElement).closest('.rm_tag_controls').find(`.rm_tag_bogus_drilldown .tag[id=${tagId}]`).remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runTagFilters(listElement);
|
runTagFilters(listElement);
|
||||||
updateTagFilterIndicator();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTagThreeState(element, { stateOverride = undefined, simulateClick = false } = {}) {
|
function toggleTagThreeState(element, { stateOverride = undefined, simulateClick = false } = {}) {
|
||||||
const states = Object.keys(FILTER_STATES);
|
const states = Object.keys(FILTER_STATES);
|
||||||
|
|
||||||
|
// Make it clear we're getting indexes and handling the 'not found' case in one place
|
||||||
|
function getStateIndex(key, fallback) {
|
||||||
|
const index = states.indexOf(key);
|
||||||
|
return index !== -1 ? index : states.indexOf(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
const overrideKey = states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride);
|
const overrideKey = states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride);
|
||||||
|
|
||||||
const currentStateIndex = states.indexOf(element.attr('data-toggle-state')) ?? states.length - 1;
|
const currentStateIndex = getStateIndex(element.attr('data-toggle-state'), DEFAULT_FILTER_STATE);
|
||||||
const targetStateIndex = overrideKey !== undefined ? states.indexOf(overrideKey) : (currentStateIndex + 1) % states.length;
|
const targetStateIndex = overrideKey !== undefined ? getStateIndex(overrideKey, DEFAULT_FILTER_STATE) : (currentStateIndex + 1) % states.length;
|
||||||
|
|
||||||
if (simulateClick) {
|
if (simulateClick) {
|
||||||
// Calculate how many clicks are needed to go from the current state to the target state
|
// Calculate how many clicks are needed to go from the current state to the target state
|
||||||
@ -695,10 +687,8 @@ function runTagFilters(listElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printTagFilters(type = tag_filter_types.character) {
|
function printTagFilters(type = tag_filter_types.character) {
|
||||||
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
|
|
||||||
const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
|
const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
|
||||||
$(FILTER_SELECTOR).empty();
|
$(FILTER_SELECTOR).empty();
|
||||||
$(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown').empty();
|
|
||||||
|
|
||||||
// Print all action tags. (Exclude folder if that setting isn't chosen)
|
// Print all action tags. (Exclude folder if that setting isn't chosen)
|
||||||
const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id);
|
const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id);
|
||||||
@ -708,18 +698,21 @@ function printTagFilters(type = tag_filter_types.character) {
|
|||||||
printTagList($(FILTER_SELECTOR), { empty: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
printTagList($(FILTER_SELECTOR), { empty: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
||||||
|
|
||||||
const characterTagIds = Object.values(tag_map).flat();
|
const characterTagIds = Object.values(tag_map).flat();
|
||||||
const tagsToDisplay = tags
|
const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort);
|
||||||
.filter(x => characterTagIds.includes(x.id))
|
|
||||||
.sort(compareTagsForSort);
|
|
||||||
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } });
|
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } });
|
||||||
|
|
||||||
runTagFilters(FILTER_SELECTOR);
|
// Print bogus folder navigation
|
||||||
|
const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown');
|
||||||
|
bogusDrilldown.empty();
|
||||||
|
if (power_user.bogus_folders && bogusDrilldown.length > 0) {
|
||||||
|
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
|
||||||
|
const navigatedTags = filterData.selected.map(x => tags.find(t => t.id == x)).filter(x => isBogusFolder(x));
|
||||||
|
|
||||||
// Simulate clicks on all "selected" tags when we reprint, otherwise their filter gets lost. "excluded" is persisted.
|
printTagList(bogusDrilldown, { tags: navigatedTags, tagOptions: { removable: true } });
|
||||||
for (const tagId of filterData.selected) {
|
|
||||||
toggleTagThreeState($(`${FILTER_SELECTOR} .tag[id="${tagId}"]`), { stateOverride: FILTER_STATES.SELECTED, simulateClick: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runTagFilters(FILTER_SELECTOR);
|
||||||
|
|
||||||
if (power_user.show_tag_filters) {
|
if (power_user.show_tag_filters) {
|
||||||
$('.rm_tag_controls .showTagList').addClass('selected');
|
$('.rm_tag_controls .showTagList').addClass('selected');
|
||||||
$('.rm_tag_controls').find('.tag:not(.actionable)').show();
|
$('.rm_tag_controls').find('.tag:not(.actionable)').show();
|
||||||
|
Reference in New Issue
Block a user