diff --git a/tests/frontend/components/autocomplete.activeFilters.test.js b/tests/frontend/components/autocomplete.activeFilters.test.js index 594c7071..09234297 100644 --- a/tests/frontend/components/autocomplete.activeFilters.test.js +++ b/tests/frontend/components/autocomplete.activeFilters.test.js @@ -286,4 +286,93 @@ const autoComplete = new AutoComplete(input, 'loras', { showPreview: false, minC window.removeEventListener('lora-manager:setting-toggled', listener); } }); + + it('removes a stale first-run hint when the toggle is switched on while the dropdown stays open', async () => { + localStorage.removeItem('lm:activefilters-tip-dismissed'); + let enabled = false; + settingGetMock.mockImplementation((key) => { + if (key === 'loramanager.lora_active_filters_autocomplete') return enabled; + if (key === 'loramanager.autocomplete_append_comma') return false; + if (key === 'loramanager.autocomplete_auto_format') return false; + if (key === 'loramanager.autocomplete_accept_key') return 'both'; + return undefined; + }); + + fetchApiMock.mockResolvedValue({ + json: () => Promise.resolve({ success: true, relative_paths: ['models/example.safetensors'] }), + }); + + const input = document.createElement('textarea'); + input.value = 'example'; + input.selectionStart = 7; + document.body.append(input); + caretHelperInstance.getBeforeCursor.mockReturnValue('example'); + + const { AutoComplete } = await import(AUTOCOMPLETE_MODULE); + const autoComplete = new AutoComplete(input, 'loras', { + debounceDelay: 0, + showPreview: false, + minChars: 1, + }); + + const triggerShow = async () => { + input.dispatchEvent(new Event('input', { bubbles: true })); + await vi.runOnlyPendingTimersAsync(); + await vi.runOnlyPendingTimersAsync(); + await Promise.resolve(); + return autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint'); + }; + + // OFF → the enable hint is shown in the suggestions dropdown. + expect(await triggerShow()).not.toBeNull(); + + // The node's filter chip toggles the setting ON while the dropdown is + // still open (ComfyUI can keep focus in the textarea, so no blur/hide + // fires). settings.js broadcasts the setting-toggled event. + enabled = true; + window.dispatchEvent(new CustomEvent('lora-manager:setting-toggled', { + detail: { settingId: 'loramanager.lora_active_filters_autocomplete', value: true }, + })); + + // The stale OFF hint must be gone even though the dropdown never closed. + expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull(); + + // Further typing while ON must not resurrect the enable hint. + expect(await triggerShow()).toBeNull(); + }); + + it('updates the command-list footer when the toggle changes while the command list is open', async () => { + let enabled = false; + settingGetMock.mockImplementation((key) => { + if (key === 'loramanager.lora_active_filters_autocomplete') return enabled; + return undefined; + }); + + const input = document.createElement('textarea'); + input.value = '/'; + input.selectionStart = 1; + document.body.append(input); + caretHelperInstance.getBeforeCursor.mockReturnValue('/'); + + const { AutoComplete } = await import(AUTOCOMPLETE_MODULE); + const autoComplete = new AutoComplete(input, 'loras', { showPreview: false, minChars: 1 }); + input.dispatchEvent(new Event('input', { bubbles: true })); + await vi.runOnlyPendingTimersAsync(); + await Promise.resolve(); + + const footer = () => autoComplete.dropdown.querySelector('.lm-autocomplete-command-footer'); + expect(footer()).not.toBeNull(); + expect(footer().textContent).toContain('Active Filters Search: OFF'); + expect(footer().textContent).toContain('/activefilters to enable'); + + enabled = true; + window.dispatchEvent(new CustomEvent('lora-manager:setting-toggled', { + detail: { settingId: 'loramanager.lora_active_filters_autocomplete', value: true }, + })); + + expect(footer()).not.toBeNull(); + expect(footer().textContent).toContain('Active Filters Search: ON'); + expect(footer().textContent).toContain('/noactivefilters to disable'); + }); + }); diff --git a/web/comfyui/autocomplete.js b/web/comfyui/autocomplete.js index 6d2ffdfe..1e58c360 100644 --- a/web/comfyui/autocomplete.js +++ b/web/comfyui/autocomplete.js @@ -14,9 +14,11 @@ import { getAutocompleteAppendCommaPreference, getAutocompleteAutoFormatPreference, getAutocompleteAcceptKeyPreference, + LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID, getLoraActiveFiltersAutocompletePreference, getPromptTagAutocompletePreference, getTagSpaceReplacementPreference, + SETTING_TOGGLED_EVENT_NAME, setLoraManagerSettingValue, } from "./settings.js"; import { showToast } from "./utils.js"; @@ -607,6 +609,7 @@ class AutoComplete { this.onBlur = null; this.onDocumentClick = null; this.onScroll = null; + this.onSettingToggled = null; this.init(); } @@ -774,6 +777,22 @@ class AutoComplete { }; document.addEventListener('click', this.onDocumentClick); + // React to setting changes that happen underneath an open dropdown + // (e.g. toggling the active-filters search from the node's filter + // chip can keep the dropdown open in ComfyUI). Refresh the state + // hints so they never show a message for the previous state. + if (this.onSettingToggled) { + window.removeEventListener(SETTING_TOGGLED_EVENT_NAME, this.onSettingToggled); + } + this.onSettingToggled = (e) => { + const detail = e && e.detail; + if (!detail || detail.settingId !== LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID) { + return; + } + this._refreshStateHints(); + }; + window.addEventListener(SETTING_TOGGLED_EVENT_NAME, this.onSettingToggled); + // Mark this element as having autocomplete events bound this.inputElement._autocompleteEventsBound = true; @@ -1811,15 +1830,37 @@ class AutoComplete { } } + /** + * Refresh state-dependent dropdown decorations (first-run Tip and the + * slash-command-list footer) from the live setting. Called when the + * active-filters toggle changes underneath an open dropdown, so the + * dropdown never keeps showing a message for the previous state. + */ + _refreshStateHints() { + if (!this.isVisible || this.modelType !== 'loras') { + return; + } + if (this.showingCommands) { + // Command list: footer text carries the ON/OFF state. + this._renderCommandListFooter(); + } else { + // Plain suggestions: the first-run Tip advertises /activefilters + // only while the feature is disabled. + this._maybeShowFirstRunHint(); + } + } + /** * Show a one-time, dismissible hint inside the dropdown surfacing the * toggle commands: prompt nodes advertise /noautocomplete, loras nodes * advertise /activefilters. Dismissal is persisted in localStorage. */ _maybeShowFirstRunHint() { - if (this.firstRunHint) { - return; - } + // Dropdown decorations can outlive a state change (the dropdown may + // stay open when the setting is toggled from the node, e.g. clicking + // the active-filters filter chip). Always recompute from the live + // state so a stale Tip is never kept for the previous state. + this._removeFirstRunHint(); let hintText = null; let storageKey = null; @@ -3178,6 +3219,11 @@ class AutoComplete { this.onDocumentClick = null; } + if (this.onSettingToggled) { + window.removeEventListener(SETTING_TOGGLED_EVENT_NAME, this.onSettingToggled); + this.onSettingToggled = null; + } + if (this.onScroll && this.scrollContainer) { this.scrollContainer.removeEventListener('scroll', this.onScroll); this.onScroll = null;