feat(ui): improve /activefilters discoverability on loras nodes

Mirror the /noautocomplete discoverability pattern for the loras\nactive-filters search toggle:\n\n- autocomplete.js: extend the slash-command-list footer and the\n  one-time first-run hint to loras nodes, advertising\n  /activefilters and /noactivefilters\n- lora_loader.js: add an 'Active Filters Search: ON/OFF' entry to the\n  right-click menu of all loras-autocomplete node classes\n- settings.js: broadcast a 'lora-manager:setting-toggled' window event\n  on every setLoraManagerSettingValue write\n- AutocompleteTextWidget.vue: add a persistent filter indicator chip\n  (loras mode only) that reflects and toggles the setting and stays in\n  sync via the setting-toggled event\n- tests: footer/hint/event coverage, context-menu tests for all four\n  node classes, widget indicator tests; rebuild vue-widgets bundle
This commit is contained in:
Will Miao
2026-09-03 22:42:45 +08:00
parent 03569c62df
commit 6ba64ebb3c
9 changed files with 860 additions and 69 deletions
@@ -23,6 +23,18 @@
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<button
v-if="isLorasMode"
type="button"
class="active-filters-toggle"
:class="{ 'is-active': activeFiltersEnabled }"
:title="activeFiltersToggleTitle"
@click="toggleActiveFiltersSearch"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
</svg>
</button>
</div>
</div>
</template>
@@ -30,6 +42,8 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useAutocomplete } from '@/composables/useAutocomplete'
// @ts-ignore - ComfyUI external module
import { LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID, SETTING_TOGGLED_EVENT_NAME, getLoraActiveFiltersAutocompletePreference, setLoraManagerSettingValue } from '../../../web/comfyui/settings.js'
// Access LiteGraph global for initial mode detection
declare const LiteGraph: { vueNodesMode?: boolean } | undefined
@@ -67,6 +81,48 @@ const hasText = ref(false)
// Show clear button when there is text
const showClearButton = computed(() => hasText.value)
// Active-filters search indicator (loras nodes only). Mirrors the
// loramanager.lora_active_filters_autocomplete setting so users can
// discover and toggle the /activefilters mode without opening the
// dropdown or the settings dialog.
const isLorasMode = (props.modelType ?? 'loras') === 'loras'
const activeFiltersEnabled = ref(false)
const refreshActiveFiltersState = () => {
if (isLorasMode) {
activeFiltersEnabled.value = getLoraActiveFiltersAutocompletePreference()
}
}
const onSettingToggled = (event: Event) => {
const detail = (event as CustomEvent<{ settingId?: string; value?: unknown }>).detail
if (detail?.settingId === LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID) {
activeFiltersEnabled.value = detail.value === true
}
}
const activeFiltersToggleTitle = computed(() =>
activeFiltersEnabled.value
? 'Active Filters Search is ON: suggestions respect the LoRA Manager page filters. Click to disable, or type /noactivefilters.'
: 'Active Filters Search is OFF: suggestions search the full library. Click to enable, or type /activefilters.'
)
const toggleActiveFiltersSearch = async () => {
const newValue = !activeFiltersEnabled.value
try {
const success = await setLoraManagerSettingValue(
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
newValue
)
if (!success) {
throw new Error('settings API unavailable')
}
activeFiltersEnabled.value = newValue
} catch (error) {
console.error('[Lora Manager] Failed to toggle active filters search:', error)
}
}
// Initialize autocomplete with direct ref access
useAutocomplete(
textareaRef,
@@ -252,6 +308,11 @@ onMounted(() => {
// Setup widget.onSetValue callback
setupWidgetOnSetValue()
// Active-filters indicator: read initial state and stay in sync with
// slash-command / context-menu toggles dispatched via settings.js
refreshActiveFiltersState()
window.addEventListener(SETTING_TOGGLED_EVENT_NAME, onSettingToggled)
// Listen for custom event dispatched by main.ts
document.addEventListener('lora-manager:vue-mode-change', onModeChange)
})
@@ -277,6 +338,7 @@ onUnmounted(() => {
// Remove event listener
document.removeEventListener('lora-manager:vue-mode-change', onModeChange)
window.removeEventListener(SETTING_TOGGLED_EVENT_NAME, onSettingToggled)
})
</script>
@@ -369,6 +431,58 @@ onUnmounted(() => {
height: 12px;
}
/* Active-filters search indicator (loras nodes only) */
.active-filters-toggle {
position: absolute;
top: 3px;
right: 3px;
width: 16px;
height: 16px;
padding: 2px;
margin: 0;
border: none;
border-radius: 4px;
background: rgba(128, 128, 128, 0.25);
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
z-index: 10;
}
.active-filters-toggle:hover {
opacity: 1;
background: rgba(128, 128, 128, 0.45);
color: rgba(255, 255, 255, 0.85);
}
.active-filters-toggle.is-active {
background: rgba(59, 130, 246, 0.35);
color: #7db8ff;
opacity: 1;
}
.active-filters-toggle svg {
width: 11px;
height: 11px;
}
/* Vue DOM mode adjustments for the indicator */
.text-input.vue-dom-mode ~ .active-filters-toggle {
top: 8px;
right: 8px;
width: 20px;
height: 20px;
}
.text-input.vue-dom-mode ~ .active-filters-toggle svg {
width: 13px;
height: 13px;
}
/* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button {
right: 8px;
@@ -10,7 +10,7 @@
import { nextTick } from 'vue'
import { shallowMount } from '@vue/test-utils'
import { describe, expect, it, vi, afterEach } from 'vitest'
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import AutocompleteTextWidget from '@/components/AutocompleteTextWidget.vue'
function createMockWidget() {
@@ -134,3 +134,109 @@ describe('AutocompleteTextWidget clear button', () => {
expect(widget.callback).toHaveBeenLastCalledWith('hello world')
})
})
/**
* Tests for the active-filters search indicator (loras mode only).
*
* The small filter chip in the textarea corner mirrors the
* loramanager.lora_active_filters_autocomplete setting: it reflects the
* current state, can toggle it, and stays in sync with slash-command /
* context-menu toggles via the lora-manager:setting-toggled window event.
*/
const settingsMocks = vi.hoisted(() => ({
getPreference: vi.fn(),
setValue: vi.fn(),
}))
vi.mock('../../../web/comfyui/settings.js', () => ({
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID:
'loramanager.lora_active_filters_autocomplete',
SETTING_TOGGLED_EVENT_NAME: 'lora-manager:setting-toggled',
getLoraActiveFiltersAutocompletePreference: settingsMocks.getPreference,
setLoraManagerSettingValue: settingsMocks.setValue,
}))
const getActiveFiltersPreferenceMock = settingsMocks.getPreference
const setSettingValueMock = settingsMocks.setValue
function mountLorasWidget() {
const widget = createMockWidget()
const node = { id: 1 }
const wrapper = shallowMount(AutocompleteTextWidget, {
props: { widget, node, modelType: 'loras' },
attachTo: document.body,
})
return { wrapper, widget }
}
describe('AutocompleteTextWidget active-filters indicator', () => {
beforeEach(() => {
getActiveFiltersPreferenceMock.mockReset()
getActiveFiltersPreferenceMock.mockReturnValue(false)
setSettingValueMock.mockReset()
setSettingValueMock.mockResolvedValue(true)
})
it('renders only in loras mode', () => {
const loras = mountLorasWidget()
expect(loras.wrapper.find('.active-filters-toggle').exists()).toBe(true)
const widget = createMockWidget()
const prompt = shallowMount(AutocompleteTextWidget, {
props: { widget, node: { id: 2 }, modelType: 'prompt' },
attachTo: document.body,
})
expect(prompt.find('.active-filters-toggle').exists()).toBe(false)
})
it('reflects the current setting state', async () => {
const { wrapper } = mountLorasWidget()
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).not.toContain('is-active')
getActiveFiltersPreferenceMock.mockReturnValue(true)
const wrapper2 = mountLorasWidget().wrapper
await nextTick()
expect(wrapper2.find('.active-filters-toggle').classes()).toContain('is-active')
})
it('toggles the setting when clicked', async () => {
const { wrapper } = mountLorasWidget()
await nextTick()
await wrapper.find('.active-filters-toggle').trigger('click')
expect(setSettingValueMock).toHaveBeenCalledWith(
'loramanager.lora_active_filters_autocomplete',
true
)
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).toContain('is-active')
})
it('stays in sync with setting-toggled window events', async () => {
const { wrapper } = mountLorasWidget()
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).not.toContain('is-active')
window.dispatchEvent(
new CustomEvent('lora-manager:setting-toggled', {
detail: {
settingId: 'loramanager.lora_active_filters_autocomplete',
value: true,
},
})
)
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).toContain('is-active')
window.dispatchEvent(
new CustomEvent('lora-manager:setting-toggled', {
detail: { settingId: 'loramanager.some_other_setting', value: true },
})
)
await nextTick()
// Unrelated settings must not flip the indicator
expect(wrapper.find('.active-filters-toggle').classes()).toContain('is-active')
})
})