fix: login redirection to active objects (#15366)

Fixes - https://github.com/twentyhq/twenty/issues/15364

- Removed `activeNonSystemObjectMetadataItems` from the hook as it was
not necessary. Hook use `alphaSortedActiveNonSystemObjectMetadataItems`
now.
- Correctly check for read permissions.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Ranjeet Baraik
2025-10-30 17:31:26 +01:00
committed by GitHub
co-authored by Charles Bochet
parent 51fdaf2731
commit 0cbd1e8ec5
12 changed files with 87 additions and 55 deletions
@@ -12,6 +12,7 @@ import {
useEventTracker,
} from '@/analytics/hooks/useEventTracker';
import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksOnAnyLocationChange';
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
@@ -88,6 +89,10 @@ export const PageChangeEffect = () => {
const { executeTasksOnAnyLocationChange } =
useExecuteTasksOnAnyLocationChange();
const isAppEffectRedirectEnabled = useRecoilValue(
isAppEffectRedirectEnabledState,
);
const { closeCommandMenu } = useCommandMenu();
const { resetFocusStackToFocusItem } = useResetFocusStackToFocusItem();
@@ -110,10 +115,18 @@ export const PageChangeEffect = () => {
useEffect(() => {
initializeQueryParamState();
if (isDefined(pageChangeEffectNavigateLocation)) {
if (
isDefined(pageChangeEffectNavigateLocation) &&
isAppEffectRedirectEnabled
) {
navigate(pageChangeEffectNavigateLocation);
}
}, [navigate, pageChangeEffectNavigateLocation, initializeQueryParamState]);
}, [
navigate,
pageChangeEffectNavigateLocation,
initializeQueryParamState,
isAppEffectRedirectEnabled,
]);
useEffect(() => {
const isLeavingRecordIndexPage = !!matchPath(
@@ -0,0 +1,5 @@
import { createState } from 'twenty-ui/utilities';
export const isAppEffectRedirectEnabledState = createState<boolean>({
key: 'isAppEffectRedirectEnabledState',
defaultValue: true,
});
@@ -66,10 +66,14 @@ import { type AuthToken } from '~/generated/graphql';
import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { loginTokenState } from '../states/loginTokenState';
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
const setLoginToken = useSetRecoilState(loginTokenState);
const setIsAppEffectRedirectEnabled = useSetRecoilState(
isAppEffectRedirectEnabledState,
);
const { origin } = useOrigin();
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
@@ -317,13 +321,20 @@ export const useAuth = () => {
async (authTokens: AuthTokenPair) => {
handleSetAuthTokens(authTokens);
setIsAppEffectRedirectEnabled(false);
// TODO: We can't parallelize this yet because when loadCurrentUSer is loaded
// then UserProvider updates its children and PrefetchDataProvider is then triggered
// which requires the correct metadata to be loaded (not the mocks)
await loadCurrentUser();
await refreshObjectMetadataItems();
},
[loadCurrentUser, handleSetAuthTokens, refreshObjectMetadataItems],
[
loadCurrentUser,
handleSetAuthTokens,
refreshObjectMetadataItems,
setIsAppEffectRedirectEnabled,
],
);
const handleGetAuthTokensFromLoginToken = useCallback(
@@ -16,10 +16,8 @@ export const useDefaultHomePagePath = () => {
const currentUser = useRecoilValue(currentUserState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const {
activeNonSystemObjectMetadataItems,
alphaSortedActiveNonSystemObjectMetadataItems,
} = useFilteredObjectMetadataItems();
const { alphaSortedActiveNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const readableAlphaSortedActiveNonSystemObjectMetadataItems = useMemo(() => {
return alphaSortedActiveNonSystemObjectMetadataItems.filter((item) => {
@@ -36,11 +34,11 @@ export const useDefaultHomePagePath = () => {
const getActiveObjectMetadataItemMatchingId = useCallback(
(objectMetadataId: string) => {
return activeNonSystemObjectMetadataItems.find(
return readableAlphaSortedActiveNonSystemObjectMetadataItems.find(
(item) => item.id === objectMetadataId,
);
},
[activeNonSystemObjectMetadataItems],
[readableAlphaSortedActiveNonSystemObjectMetadataItems],
);
const getFirstView = useRecoilCallback(({ snapshot }) => {
@@ -76,20 +74,13 @@ export const useDefaultHomePagePath = () => {
.getLoadable(lastVisitedObjectMetadataItemIdState)
.getValue();
if (
!isDefined(lastVisitedObjectMetadataItemId) ||
!getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: lastVisitedObjectMetadataItemId,
}).canReadObjectRecords
) {
return firstObjectPathInfo;
}
const lastVisitedObjectMetadataItem =
getActiveObjectMetadataItemMatchingId(
lastVisitedObjectMetadataItemId,
);
const lastVisitedObjectMetadataItem = isDefined(
lastVisitedObjectMetadataItemId,
)
? getActiveObjectMetadataItemMatchingId(
lastVisitedObjectMetadataItemId,
)
: undefined;
if (isDefined(lastVisitedObjectMetadataItem)) {
return {
@@ -101,12 +92,7 @@ export const useDefaultHomePagePath = () => {
return firstObjectPathInfo;
};
},
[
firstObjectPathInfo,
getActiveObjectMetadataItemMatchingId,
getFirstView,
objectPermissionsByObjectMetadataId,
],
[firstObjectPathInfo, getActiveObjectMetadataItemMatchingId, getFirstView],
);
const defaultHomePagePath = useMemo(() => {
@@ -2,8 +2,8 @@ import React from 'react';
import { useRecoilValue } from 'recoil';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
export const ObjectMetadataItemsProvider = ({
@@ -11,12 +11,10 @@ export const ObjectMetadataItemsProvider = ({
}: React.PropsWithChildren) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const isAppWaitingForFreshObjectMetadata = useRecoilValue(
isAppWaitingForFreshObjectMetadataState,
);
const shouldAppBeLoading = useRecoilValue(shouldAppBeLoadingState);
const shouldDisplayChildren =
!isAppWaitingForFreshObjectMetadata && objectMetadataItems.length > 0;
!shouldAppBeLoading && objectMetadataItems.length > 0;
return (
<>
@@ -1,5 +1,6 @@
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { useRecoilCallback } from 'recoil';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@@ -17,12 +18,15 @@ export const useLoadMockedObjectMetadataItems = () => {
set(objectMetadataItemsState, generatedMockObjectMetadataItems);
}
if (snapshot.getLoadable(shouldAppBeLoadingState).getValue() === true) {
set(shouldAppBeLoadingState, false);
}
if (
snapshot
.getLoadable(isAppWaitingForFreshObjectMetadataState)
.getValue() === true
snapshot.getLoadable(isAppEffectRedirectEnabledState).getValue() ===
false
) {
set(isAppWaitingForFreshObjectMetadataState, false);
set(isAppEffectRedirectEnabledState, true);
}
},
[],
@@ -1,7 +1,8 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
@@ -75,7 +76,17 @@ export const useRefreshObjectMetadataItems = (
newObjectMetadataItems.length > 0
) {
set(objectMetadataItemsState, newObjectMetadataItems);
set(isAppWaitingForFreshObjectMetadataState, false);
}
if (snapshot.getLoadable(shouldAppBeLoadingState).getValue() === true) {
set(shouldAppBeLoadingState, false);
}
if (
snapshot.getLoadable(isAppEffectRedirectEnabledState).getValue() ===
false
) {
set(isAppEffectRedirectEnabledState, true);
}
return newObjectMetadataItems;
@@ -1,5 +0,0 @@
import { createState } from 'twenty-ui/utilities';
export const isAppWaitingForFreshObjectMetadataState = createState<boolean>({
key: 'isAppWaitingForFreshObjectMetadataState',
defaultValue: false,
});
@@ -0,0 +1,5 @@
import { createState } from 'twenty-ui/utilities';
export const shouldAppBeLoadingState = createState<boolean>({
key: 'shouldAppBeLoadingState',
defaultValue: false,
});
@@ -1,5 +1,5 @@
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
import { type Meta, type StoryObj } from '@storybook/react';
@@ -18,7 +18,7 @@ const meta: Meta<typeof WidgetPlaceholder> = {
objectMetadataItemsState,
generatedMockObjectMetadataItems,
);
snapshot.set(isAppWaitingForFreshObjectMetadataState, false);
snapshot.set(shouldAppBeLoadingState, false);
};
return (
@@ -9,8 +9,8 @@ import { MemoryRouter } from 'react-router-dom';
import { type MutableSnapshot } from 'recoil';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
@@ -122,7 +122,7 @@ const meta: Meta<typeof WidgetRenderer> = {
objectMetadataItemsState,
generatedMockObjectMetadataItems,
);
snapshot.set(isAppWaitingForFreshObjectMetadataState, false);
snapshot.set(shouldAppBeLoadingState, false);
};
return (
@@ -1,17 +1,21 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useAuth } from '@/auth/hooks/useAuth';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { useSetRecoilState } from 'recoil';
export const useImpersonationAuth = () => {
const { getAuthTokensFromLoginToken } = useAuth();
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
const setShouldAppBeLoading = useSetRecoilState(shouldAppBeLoadingState);
const setIsAppEffectRedirectEnabled = useSetRecoilState(
isAppEffectRedirectEnabledState,
);
const executeImpersonationAuth = async (loginToken: string) => {
setIsAppWaitingForFreshObjectMetadata(true);
setShouldAppBeLoading(true);
setIsAppEffectRedirectEnabled(false);
await getAuthTokensFromLoginToken(loginToken);
setIsAppWaitingForFreshObjectMetadata(false);
setShouldAppBeLoading(false);
setIsAppEffectRedirectEnabled(true);
};
return { executeImpersonationAuth };