diff --git a/packages/frontend/src/api/hooks/useResolvedView.ts b/packages/frontend/src/api/hooks/useResolvedView.ts index 62464cd5a..2ccf6d7b3 100644 --- a/packages/frontend/src/api/hooks/useResolvedView.ts +++ b/packages/frontend/src/api/hooks/useResolvedView.ts @@ -40,20 +40,13 @@ export const useResolvedView = (): ReturnType => { selectedView?.isReadOnly, ]); - return useGetViewByHashQuery( - { - ...(isViewHash - ? { hash: routeInfo?.value } - : // if the route is not a hash, it's a slug. - // if the route info has no slug, we'll use the selected view slug - // this should pose no issues if the selected view is not a system view - // and should work as expected if the selected view is a system view - { slug: routeInfo?.value ?? selectedView?.data.slug }), - }, - { - skip: skipViewFetch, - } - ); + const queryParams = isViewHash + ? { hash: routeInfo?.value } + : { slug: routeInfo?.value ?? selectedView?.data.slug }; + + return useGetViewByHashQuery(queryParams, { + skip: skipViewFetch, + }); }; export default useResolvedView; diff --git a/packages/frontend/src/api/hooks/useView.ts b/packages/frontend/src/api/hooks/useView.ts index fa94175e0..c24ad0229 100644 --- a/packages/frontend/src/api/hooks/useView.ts +++ b/packages/frontend/src/api/hooks/useView.ts @@ -179,15 +179,18 @@ export const useView: () => IView = () => { * We do this to show the shared views in the views tab. */ const subscribedViews = useMemo(() => { - const allViews = [ - ...Object.values(subscribedViewsCache ?? {}), - ...Object.values(sharedViewsCache ?? {}), - ].reduce((acc, view) => { - if (!acc.find((v) => v.data.id === view.data.id)) { - return [...acc, view]; - } - return acc; - }, [] as TSubscribedView[]); + const subscribedCacheViews = Object.values(subscribedViewsCache ?? {}); + const sharedCacheViews = Object.values(sharedViewsCache ?? {}); + + const allViews = [...subscribedCacheViews, ...sharedCacheViews].reduce( + (acc, view) => { + if (!acc.find((v) => v.data.id === view.data.id)) { + return [...acc, view]; + } + return acc; + }, + [] as TSubscribedView[] + ); const systemViews = allViews .filter((v) => v.data.is_system_view) .sort( @@ -198,6 +201,7 @@ export const useView: () => IView = () => { .sort( (viewA, viewD) => viewA.data.sort_order - viewD.data.sort_order ); + return [...systemViews, ...customViews]; }, [subscribedViewsCache, sharedViewsCache]); @@ -246,11 +250,35 @@ export const useView: () => IView = () => { * cache. Otherwise, we will add it to the shared views cache. * This is to ensure the views cache is always up to date. */ - if ( - // the user must be subscribed to the view + const isSubscribed = remoteSubscribedViews?.find((v) => v.id === view.id) !== - undefined - ) { + undefined; + + if (isSubscribed) { + /** + * Even though subscribed views are loaded via the remoteSubscribedViews + * useEffect, we still need to add them here because: + * 1. The resolved view data might be more recent than the cached subscription data + * 2. On first navigation, remoteSubscribedViews might not have loaded yet + * 3. This ensures the view is immediately available in both caches: + * - subscribedViewsCache (minimal metadata) for the Views Tab + * - viewsCache (full data) for sort order calculation and other operations + */ + // Extract only preview fields (without widgets, keywords, max_widgets, language) + const viewPreview: TRemoteUserViewPreview = { + id: view.id, + hash: view.hash, + name: view.name, + slug: view.slug, + icon: view.icon, + description: view.description, + is_subscribed: view.is_subscribed, + is_system_view: view.is_system_view, + is_smart: view.is_smart, + sort_order: view.sort_order, + updated_at: view.updated_at, + }; + dispatch(viewsStore.updateSubscribedViewsCache([viewPreview])); dispatch( viewsStore.setViewsCache({ ...viewsCache, diff --git a/packages/frontend/src/api/hooks/useViewUpdater.ts b/packages/frontend/src/api/hooks/useViewUpdater.ts index 9f33194a9..70e515e33 100644 --- a/packages/frontend/src/api/hooks/useViewUpdater.ts +++ b/packages/frontend/src/api/hooks/useViewUpdater.ts @@ -131,18 +131,23 @@ export const useViewUpdater: () => void = () => { }); const shouldAddToCache = useMemo(() => { - if (!resolvedView.currentData) return false; + if (!resolvedView.currentData) { + return false; + } const isLangChanged = isViewLangModified( selectedView, resolvedView.currentData ); + + const foundInAvailableViews = availableViews?.find( + (v) => v.data.hash === resolvedView.data?.hash + ); + const isNewView = !isViewModified && !isFetchingSubscribedViews && - !availableViews?.find( - (v) => v.data.hash === resolvedView.data?.hash - ); + !foundInAvailableViews; return isLangChanged || isNewView; }, [ @@ -155,10 +160,6 @@ export const useViewUpdater: () => void = () => { ]); if (shouldAddToCache && resolvedView.currentData) { - Logger.debug( - "useViewUpdater: adding view to cache", - resolvedView.currentData.name - ); addViewToCache(resolvedView.currentData); } @@ -281,7 +282,6 @@ export const useViewUpdater: () => void = () => { const viewFromPathInCache = useMemo(() => { if (routeInfo === undefined) return undefined; - // Look for the current view slug/hash in the views cache return subscribedViews.find( (v) => v.data.slug === routeInfo.value || @@ -299,6 +299,7 @@ export const useViewUpdater: () => void = () => { routeInfo !== undefined && !resolvedView.isError ) { + // First, try to select from cache if available if ( viewFromPathInCache !== undefined && viewFromPathInCache.data.id !== selectedViewId @@ -309,6 +310,18 @@ export const useViewUpdater: () => void = () => { setSelectedViewId(viewFromPathInCache.data.id); return; } + + // If not in cache but successfully resolved from backend, select it directly + // This handles the case where a view is resolved but not yet reflected in the + // subscribedViews cache (e.g., shared/public boards on first visit) + if ( + viewFromPathInCache === undefined && + resolvedView.currentData !== undefined && + resolvedView.currentData.id !== selectedViewId + ) { + setSelectedViewId(resolvedView.currentData.id); + return; + } } /** * If resolving the view from the path fails, we should reset the selectedViewId.