diff --git a/frontend/src/composables/useSearchReset.ts b/frontend/src/composables/useSearchReset.ts deleted file mode 100644 index d53cec428..000000000 --- a/frontend/src/composables/useSearchReset.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { storeToRefs } from 'pinia'; -import { onActivated, onDeactivated } from 'vue'; -import GlobalStore from '@/store/modules/global'; -import { bindSearchState, type SearchFields } from '@/utils/search-reset'; - -interface SearchResetOptions { - scope?: () => string | number | undefined; - onRevisit: () => unknown; -} - -/** Retain the existing entry point while keeping queries across all page visits. */ -export const useSearchReset = (page: string, fields: SearchFields, options: SearchResetOptions) => { - const { currentNode } = storeToRefs(GlobalStore()); - const restore = bindSearchState(() => { - const scope = options.scope?.(); - if (options.scope && scope === undefined) return undefined; - return JSON.stringify([currentNode.value, page, scope ?? '']); - }, fields); - let deactivated = false; - onDeactivated(() => { - deactivated = true; - }); - onActivated(() => { - if (!deactivated) return; - deactivated = false; - restore(); - return options.onRevisit(); - }); -}; diff --git a/frontend/src/utils/search-reset.ts b/frontend/src/utils/search-reset.ts index d341299e6..c85a04d3f 100644 --- a/frontend/src/utils/search-reset.ts +++ b/frontend/src/utils/search-reset.ts @@ -1,27 +1,30 @@ -import { watch, type Ref } from 'vue'; +import { storeToRefs } from 'pinia'; +import { onActivated, onDeactivated, onScopeDispose, watch, type Ref } from 'vue'; +import GlobalStore from '@/store/modules/global'; export type SearchFields = Record>; type SearchValues = Record; /** Shared by normal page visits and edit-return visits; navigation never evicts a query. */ -export const createSearchVisitCache = (storage: () => Storage = () => localStorage) => { - const entries = new Map(); +export const createSearchStateStorage = (storage: () => Storage = () => localStorage) => { + const unsaved = new Map(); return { save(key: string, values: SearchValues) { - entries.set(key, { ...values }); try { - storage().setItem( - `1panel:search-state:v2:${key}`, - JSON.stringify( - Object.fromEntries(Object.entries(values).map(([name, value]) => [name, value ?? null])), - ), + const target = storage(); + const storageKey = `1panel:search-state:v2:${key}`; + const serialized = JSON.stringify( + Object.fromEntries(Object.entries(values).map(([name, value]) => [name, value ?? null])), ); + if (target.getItem(storageKey) !== serialized) target.setItem(storageKey, serialized); + unsaved.delete(key); } catch { - // Keep in-memory state when storage is unavailable or full. + // Only failed writes need an in-memory fallback. + unsaved.set(key, { ...values }); } }, read(key: string): SearchValues | undefined { - const cached = entries.get(key); + const cached = unsaved.get(key); if (cached) return { ...cached }; try { const saved = JSON.parse(storage().getItem(`1panel:search-state:v2:${key}`) || 'null'); @@ -43,20 +46,31 @@ export const createSearchVisitCache = (storage: () => Storage = () => localStora }; }; -const visitCache = createSearchVisitCache(); +const searchStorage = createSearchStateStorage(); -export const bindSearchState = (key: () => string | undefined, fields: SearchFields, cache = visitCache) => { +export const bindSearchState = (key: () => string | undefined, fields: SearchFields, storage = searchStorage) => { const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value])); + const names = Object.keys(fields).sort((left, right) => Number(left === 'page') - Number(right === 'page')); + let pending: { key: string; values: SearchValues } | undefined; + let queued = false; + const flush = () => { + if (!pending) return; + const snapshot = pending; + pending = undefined; + storage.save(snapshot.key, snapshot.values); + }; + onScopeDispose(flush); let restoring = false; let activeKey: string | undefined; const restore = () => { + // Commit the previous node/resource before restoring another one. + flush(); activeKey = key(); if (!activeKey) return; - const saved = cache.read(activeKey) || defaults; + const saved = storage.read(activeKey) || defaults; restoring = true; try { // Restore pagination last: search/filter watchers may reset it to page one. - const names = Object.keys(fields).sort((left, right) => Number(left === 'page') - Number(right === 'page')); for (const name of names) { const value = Object.hasOwn(saved, name) ? saved[name] : defaults[name]; const initial = defaults[name]; @@ -71,9 +85,39 @@ export const bindSearchState = (key: () => string | undefined, fields: SearchFie watch( () => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value])), (values) => { - if (!restoring && activeKey) cache.save(activeKey, values); + if (restoring || !activeKey) return; + pending = { key: activeKey, values }; + if (queued) return; + queued = true; + queueMicrotask(() => { + queued = false; + flush(); + }); }, - { immediate: true, flush: 'sync' }, + { flush: 'sync' }, ); - return restore; +}; + +interface SearchStateOptions { + scope?: () => string | number | undefined; + onRevisit: () => unknown; +} + +/** Keep queries across page visits and reload data when a cached page becomes active. */ +export const useSearchState = (page: string, fields: SearchFields, options: SearchStateOptions) => { + const { currentNode } = storeToRefs(GlobalStore()); + bindSearchState(() => { + const scope = options.scope?.(); + if (options.scope && scope === undefined) return undefined; + return JSON.stringify([currentNode.value, page, scope ?? '']); + }, fields); + let deactivated = false; + onDeactivated(() => { + deactivated = true; + }); + onActivated(() => { + if (!deactivated) return; + deactivated = false; + return options.onRevisit(); + }); }; diff --git a/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue b/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue index e7e072c12..adb704b14 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/plugins.vue @@ -172,7 +172,7 @@