[Manager] Persist/Restore Manager UI state (#4180)

This commit is contained in:
Christian Byrne
2025-06-14 15:19:56 -07:00
committed by GitHub
parent 23ba7e6501
commit 5b32d2aad0
6 changed files with 119 additions and 41 deletions

View File

@@ -93,7 +93,14 @@
import { whenever } from '@vueuse/core'
import { merge } from 'lodash'
import Button from 'primevue/button'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import {
computed,
onBeforeUnmount,
onMounted,
onUnmounted,
ref,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -106,6 +113,7 @@ import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useManagerStatePersistence } from '@/composables/manager/useManagerStatePersistence'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
@@ -116,13 +124,15 @@ import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
const { initialTab = ManagerTab.All } = defineProps<{
initialTab: ManagerTab
const { initialTab } = defineProps<{
initialTab?: ManagerTab
}>()
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
const { getPackById } = useComfyRegistryStore()
const persistedState = useManagerStatePersistence()
const initialState = persistedState.loadStoredState()
const GRID_STYLE = {
display: 'grid',
@@ -156,8 +166,10 @@ const tabs = ref<TabItem[]>([
icon: 'pi-sync'
}
])
const initialTabId = initialTab ?? initialState.selectedTabId
const selectedTab = ref<TabItem>(
tabs.value.find((tab) => tab.id === initialTab) || tabs.value[0]
tabs.value.find((tab) => tab.id === initialTabId) || tabs.value[0]
)
const {
@@ -168,7 +180,11 @@ const {
searchMode,
sortField,
suggestions
} = useRegistrySearch()
} = useRegistrySearch({
initialSortField: initialState.sortField,
initialSearchMode: initialState.searchMode,
initialSearchQuery: initialState.searchQuery
})
pageNumber.value = 0
const onApproachEnd = () => {
pageNumber.value++
@@ -456,6 +472,15 @@ watch(searchQuery, () => {
}
})
onBeforeUnmount(() => {
persistedState.persistState({
selectedTabId: selectedTab.value?.id,
searchQuery: searchQuery.value,
searchMode: searchMode.value,
sortField: sortField.value
})
})
onUnmounted(() => {
getPackById.cancel()
})

View File

@@ -0,0 +1,54 @@
import {
ManagerState,
ManagerTab,
SortableAlgoliaField
} from '@/types/comfyManagerTypes'
const STORAGE_KEY = 'Comfy.Manager.UI.State'
export const useManagerStatePersistence = () => {
/**
* Load the UI state from localStorage.
*/
const loadStoredState = (): ManagerState => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
return JSON.parse(stored)
}
} catch (e) {
console.error('Failed to load manager UI state:', e)
}
return {
selectedTabId: ManagerTab.All,
searchQuery: '',
searchMode: 'packs',
sortField: SortableAlgoliaField.Downloads
}
}
/**
* Persist the UI state to localStorage.
*/
const persistState = (state: ManagerState) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
}
/**
* Reset the UI state to the default values.
*/
const reset = () => {
persistState({
selectedTabId: ManagerTab.All,
searchQuery: '',
searchMode: 'packs',
sortField: SortableAlgoliaField.Downloads
})
}
return {
loadStoredState,
persistState,
reset
}
}

View File

@@ -1,7 +1,7 @@
import { watchDebounced } from '@vueuse/core'
import type { Hit } from 'algoliasearch/dist/lite/browser'
import { memoize, orderBy } from 'lodash'
import { computed, onUnmounted, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useAlgoliaSearchService } from '@/services/algoliaSearchService'
import type {
@@ -14,7 +14,6 @@ import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
const SEARCH_DEBOUNCE_TIME = 320
const DEFAULT_PAGE_SIZE = 64
const DEFAULT_SORT_FIELD = SortableAlgoliaField.Downloads // Set in the index configuration
const DEFAULT_MAX_CACHE_SIZE = 64
const SORT_DIRECTIONS: Record<SortableAlgoliaField, 'asc' | 'desc'> = {
[SortableAlgoliaField.Downloads]: 'desc',
[SortableAlgoliaField.Created]: 'desc',
@@ -30,18 +29,25 @@ const isDateField = (field: SortableAlgoliaField): boolean =>
/**
* Composable for managing UI state of Comfy Node Registry search.
*/
export function useRegistrySearch(
options: {
maxCacheSize?: number
} = {}
) {
const { maxCacheSize = DEFAULT_MAX_CACHE_SIZE } = options
export function useRegistrySearch(options: {
initialSortField?: SortableAlgoliaField
initialSearchMode?: 'nodes' | 'packs'
initialSearchQuery?: string
initialPageNumber?: number
}) {
const {
initialSortField = SortableAlgoliaField.Downloads,
initialSearchMode = 'packs',
initialSearchQuery = '',
initialPageNumber = 0
} = options
const isLoading = ref(false)
const sortField = ref<SortableAlgoliaField>(SortableAlgoliaField.Downloads)
const searchMode = ref<'nodes' | 'packs'>('packs')
const sortField = ref<SortableAlgoliaField>(initialSortField)
const searchMode = ref<'nodes' | 'packs'>(initialSearchMode)
const pageSize = ref(DEFAULT_PAGE_SIZE)
const pageNumber = ref(0)
const searchQuery = ref('')
const pageNumber = ref(initialPageNumber)
const searchQuery = ref(initialSearchQuery)
const results = ref<AlgoliaNodePack[]>([])
const suggestions = ref<NodesIndexSuggestion[]>([])
@@ -62,9 +68,7 @@ export function useRegistrySearch(
)
const { searchPacksCached, toRegistryPack, clearSearchPacksCache } =
useAlgoliaSearchService({
maxCacheSize
})
useAlgoliaSearchService()
const algoliaToRegistry = memoize(
toRegistryPack,
@@ -124,8 +128,6 @@ export function useRegistrySearch(
immediate: true
})
onUnmounted(clearSearchPacksCache)
return {
isLoading,
pageNumber,
@@ -135,6 +137,7 @@ export function useRegistrySearch(
searchQuery,
suggestions,
searchResults: resultsAsRegistryPacks,
nodeSearchResults: resultsAsNodes
nodeSearchResults: resultsAsNodes,
clearCache: clearSearchPacksCache
}
}

View File

@@ -40,12 +40,6 @@ const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
]
interface AlgoliaSearchServiceOptions {
/**
* Maximum number of search results to store in the cache.
* The cache is automatically cleared when the component is unmounted.
* @default 64
*/
maxCacheSize?: number
/**
* Minimum number of characters for suggestions. An additional query
* will be made to the suggestions/completions index for queries that
@@ -55,17 +49,15 @@ interface AlgoliaSearchServiceOptions {
minCharsForSuggestions?: number
}
const searchPacksCache = new QuickLRU<string, SearchPacksResult>({
maxSize: DEFAULT_MAX_CACHE_SIZE
})
export const useAlgoliaSearchService = (
options: AlgoliaSearchServiceOptions = {}
) => {
const {
maxCacheSize = DEFAULT_MAX_CACHE_SIZE,
minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS
} = options
const { minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS } = options
const searchClient = algoliasearch(__ALGOLIA_APP_ID__, __ALGOLIA_API_KEY__)
const searchPacksCache = new QuickLRU<string, SearchPacksResult>({
maxSize: maxCacheSize
})
const toRegistryLatestVersion = (
algoliaNode: AlgoliaNodePack

View File

@@ -21,7 +21,6 @@ import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkfl
import { t } from '@/i18n'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
export type ConfirmationDialogType =
| 'default'
@@ -129,9 +128,7 @@ export const useDialogService = () => {
}
function showManagerDialog(
props: InstanceType<typeof ManagerDialogContent>['$props'] = {
initialTab: ManagerTab.All
}
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
) {
dialogStore.showDialog({
key: 'global-manager',

View File

@@ -39,7 +39,7 @@ export enum SortableAlgoliaField {
}
export interface TabItem {
id: string
id: ManagerTab
label: string
icon: string
}
@@ -234,3 +234,10 @@ export interface InstallPackParams extends ManagerPackInfo {
export interface UpdateAllPacksParams {
mode?: ManagerDatabaseSource
}
export interface ManagerState {
selectedTabId: ManagerTab
searchQuery: string
searchMode: 'nodes' | 'packs'
sortField: SortableAlgoliaField
}