Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions packages/frontend/src/api/hooks/useResolvedView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,13 @@ export const useResolvedView = (): ReturnType<typeof useGetViewByHashQuery> => {
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;
54 changes: 41 additions & 13 deletions packages/frontend/src/api/hooks/useView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,18 @@ export const useView: () => IView = () => {
* We do this to show the shared views in the views tab.
*/
const subscribedViews = useMemo<TSubscribedView[]>(() => {
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(
Expand All @@ -198,6 +201,7 @@ export const useView: () => IView = () => {
.sort(
(viewA, viewD) => viewA.data.sort_order - viewD.data.sort_order
);

return [...systemViews, ...customViews];
}, [subscribedViewsCache, sharedViewsCache]);

Expand Down Expand Up @@ -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,
Expand Down
31 changes: 22 additions & 9 deletions packages/frontend/src/api/hooks/useViewUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}, [
Expand All @@ -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);
}

Expand Down Expand Up @@ -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 ||
Expand All @@ -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
Expand All @@ -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.
Expand Down
Loading