From df469db74d11c8d47b913e7b7bae7b5c8482d6bc Mon Sep 17 00:00:00 2001 From: shariquerik Date: Thu, 16 Jul 2026 17:15:09 +0530 Subject: [PATCH 1/4] feat: cache API resource data across page loads Adds a "Cache data across page loads" option for API Resources, so repeat visits to a page can reuse previously fetched data instead of refetching, keyed on the resource's url, method, and params. --- frontend/src/components/ResourceDialog.vue | 8 ++++++++ frontend/src/data/studioResources.ts | 1 + frontend/src/stores/codeStore.ts | 11 +++++++++-- frontend/src/types/Studio/StudioResource.ts | 2 ++ .../studio_page_resource/studio_page_resource.json | 8 ++++++++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ResourceDialog.vue b/frontend/src/components/ResourceDialog.vue index ddbe4942d..6b1d92faa 100644 --- a/frontend/src/components/ResourceDialog.vue +++ b/frontend/src/components/ResourceDialog.vue @@ -144,6 +144,13 @@ + + ({ ...emptyResource }) diff --git a/frontend/src/data/studioResources.ts b/frontend/src/data/studioResources.ts index 775e4e808..bd3cc0381 100644 --- a/frontend/src/data/studioResources.ts +++ b/frontend/src/data/studioResources.ts @@ -7,6 +7,7 @@ export const studioPageResources = createListResource({ "resource_type", "resource_name", "auto", + "cache", "fields", "filters", "limit", diff --git a/frontend/src/stores/codeStore.ts b/frontend/src/stores/codeStore.ts index ba7e836ce..c452af5c4 100644 --- a/frontend/src/stores/codeStore.ts +++ b/frontend/src/stores/codeStore.ts @@ -13,7 +13,7 @@ import * as globalUtils from "@/utils/globalUtils" import { getInitialVariableValue, getValueFromObject, setValueInObject } from "@/utils/helpers" import { isDynamicValue, normalizeDynamicValue } from "@/utils/code" import { isFunctionExpression, toOptionalChaining, getTopLevelBindings } from "@/utils/parseCode" -import type { Filters, Resource, DocumentResource, DataResult } from "@/types/Studio/StudioResource" +import type { Filters, Resource, APIResource, DocumentResource, DataResult } from "@/types/Studio/StudioResource" import type { StudioPage } from "@/types/Studio/StudioPage" import type { Variable } from "@/types/Studio/StudioPageVariable" import type { ExpressionEvaluationContext } from "@/types" @@ -464,17 +464,24 @@ const useCodeStore = defineStore("codeStore", () => { } return createListResource(params) case "API Resource": + const apiParams = getAPIParams(resource.params, context) return createResource({ url: resource.url, method: resource.method, - params: getAPIParams(resource.params, context), + params: apiParams, auto: resource.auto, + cache: getCacheOption(resource, apiParams), ...getTransforms(resource), ...getSuccessErrorHandlers(resource), }) } } + function getCacheOption(resource: APIResource, params: Record | string | null) { + if (!resource.cache) return undefined + return [resource.resource_name, resource.url, resource.method, JSON.stringify(params)] + } + function getAPIParams(params: Record | string | null = null, context: ExpressionEvaluationContext) { if (!params) return null if (typeof params === "string") { diff --git a/frontend/src/types/Studio/StudioResource.ts b/frontend/src/types/Studio/StudioResource.ts index 0c6e7c931..a71f9ee8a 100644 --- a/frontend/src/types/Studio/StudioResource.ts +++ b/frontend/src/types/Studio/StudioResource.ts @@ -9,6 +9,8 @@ interface BaseResource { resource_type: ResourceType /** Whether to automatically fetch data on first load */ auto?: boolean + /** Whether to reuse the resource and its data across page loads, keyed on url + params */ + cache?: boolean transform?: string | null on_success?: string on_error?: string diff --git a/studio/studio/doctype/studio_page_resource/studio_page_resource.json b/studio/studio/doctype/studio_page_resource/studio_page_resource.json index 3494c9c17..47016e351 100644 --- a/studio/studio/doctype/studio_page_resource/studio_page_resource.json +++ b/studio/studio/doctype/studio_page_resource/studio_page_resource.json @@ -14,6 +14,7 @@ "sort_field", "sort_order", "auto", + "cache", "document_resource_section", "document_type", "column_break_mkdu", @@ -173,6 +174,13 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Auto fetch data on load" + }, + { + "default": "0", + "description": "Reuse this resource and its data across page loads, keyed on url + params", + "fieldname": "cache", + "fieldtype": "Check", + "label": "Cache data across page loads" } ], "grid_page_length": 50, From 3f84074ef7856b5c6e987d72bd5195ffbcae80ac Mon Sep 17 00:00:00 2001 From: shariquerik Date: Thu, 16 Jul 2026 17:21:08 +0530 Subject: [PATCH 2/4] fix: scope switch-case declarations with block braces Fixes no-case-declarations lint violation in getResource by wrapping the Document List and API Resource case bodies in blocks. --- frontend/src/stores/codeStore.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/stores/codeStore.ts b/frontend/src/stores/codeStore.ts index c452af5c4..469dfe2e3 100644 --- a/frontend/src/stores/codeStore.ts +++ b/frontend/src/stores/codeStore.ts @@ -449,7 +449,7 @@ const useCodeStore = defineStore("codeStore", () => { switch (resource.resource_type) { case "Document": return getDocumentResource(resource, context) - case "Document List": + case "Document List": { const params: any = { doctype: resource.document_type, fields: fields.length ? fields : "*", @@ -463,7 +463,8 @@ const useCodeStore = defineStore("codeStore", () => { params["orderBy"] = `${resource.sort_field} ${resource.sort_order}` } return createListResource(params) - case "API Resource": + } + case "API Resource": { const apiParams = getAPIParams(resource.params, context) return createResource({ url: resource.url, @@ -474,6 +475,7 @@ const useCodeStore = defineStore("codeStore", () => { ...getTransforms(resource), ...getSuccessErrorHandlers(resource), }) + } } } From 47b2518ad294b909c461bfd674c604acc13e1327 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 20 Jul 2026 12:01:08 +0530 Subject: [PATCH 3/4] fix: avoid clearing resources while new ones resolve setPageResources reset resources to {} before awaiting the resource promises, so components rendered against an empty map for the duration of the fetch. With caching enabled that turned an instant cache hit into a flash of empty state. Build the map locally and swap it in once. --- frontend/src/stores/codeStore.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/stores/codeStore.ts b/frontend/src/stores/codeStore.ts index 469dfe2e3..7a49248a2 100644 --- a/frontend/src/stores/codeStore.ts +++ b/frontend/src/stores/codeStore.ts @@ -55,7 +55,6 @@ const useCodeStore = defineStore("codeStore", () => { async function setPageResources(page: StudioPage, setResourceConfig: boolean = false) { studioPageResources.filters = { parent: page.name } await studioPageResources.reload() - resources.value = {} const resourcePromises = studioPageResources.data.map(async (resource: Resource) => { const newResource = await getNewResource(resource, { @@ -73,14 +72,16 @@ const useCodeStore = defineStore("codeStore", () => { const resolvedResources = await Promise.all(resourcePromises) + const nextResources: Record = {} resolvedResources.forEach((item) => { - resources.value[item.resource_name] = item.value + nextResources[item.resource_name] = item.value if (setResourceConfig) { if (!item.value) return - resources.value[item.resource_name].resource_id = item.resource_id - resources.value[item.resource_name].resource_type = item.resource_type + nextResources[item.resource_name].resource_id = item.resource_id + nextResources[item.resource_name].resource_type = item.resource_type } }) + resources.value = nextResources } async function setPageVariables(page: StudioPage) { From 0435b9c1ea8e299d4b92dd93502c4eb944ae8a75 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 20 Jul 2026 12:01:08 +0530 Subject: [PATCH 4/4] fix: keep the block tree mounted on same-page navigation The route watcher rebuilt rootBlock from page.blocks on every path change, so navigating between routes served by the same Studio page (e.g. a list and its detail route, or a view switch) reinstantiated the whole tree and destroyed component state. Track the last rendered path: a same-page navigation still refreshes resources and the page script, since route params may have changed, but leaves the existing blocks in place. Path resolution moves into resolvePagePath() and the no-path case returns early. --- frontend/src/pages/AppContainer.vue | 36 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/AppContainer.vue b/frontend/src/pages/AppContainer.vue index 850baedca..cdacc9836 100644 --- a/frontend/src/pages/AppContainer.vue +++ b/frontend/src/pages/AppContainer.vue @@ -24,39 +24,47 @@ const codeStore = useCodeStore() const page = ref(null) const rootBlock = ref(null) +let renderedPath: string | null = null -async function loadPage() { - let { pageRoute } = route.params as { pageRoute: string[] } - const isDynamic = route.meta?.isDynamic - - let currentPath = "/" - if (isDynamic) { - currentPath = route.matched?.[0]?.path - } else if (pageRoute) { - currentPath = pageRoute[0] - } - +async function loadPage(force: boolean = false) { + const currentPath = resolvePagePath() if (!currentPath) { rootBlock.value = null + renderedPath = null return } - page.value = await findPageWithRoute(window.app_name, currentPath) + const isSamePage = !force && currentPath === renderedPath + if (!isSamePage) { + const nextPage = await findPageWithRoute(window.app_name, currentPath) + if (!nextPage) return + page.value = nextPage + } if (!page.value) return + await store.setPageData(page.value) await codeStore.setPageScript(page.value, Boolean(page.value.is_standard)) + if (isSamePage) return + const blocks = window.is_preview ? JSON.parse(page.value?.draft_blocks || page.value?.blocks) : JSON.parse(page.value?.blocks) if (blocks) { rootBlock.value = getBlockInstance(blocks[0]) + renderedPath = currentPath } } -watch(() => route.path, loadPage, { immediate: true }) +watch(() => route.path, () => loadPage(), { immediate: true }) -if (window.is_preview) useLivePreview(page, loadPage) +if (window.is_preview) useLivePreview(page, () => loadPage(true)) + +function resolvePagePath() { + if (route.meta?.isDynamic) return route.matched?.[0]?.path + const { pageRoute } = route.params as { pageRoute: string[] } + return pageRoute ? pageRoute[0] : "/" +} usePageMeta(() => { return {