Add viewId to recordIndexId (#9647)
Before the `recordIndexId` was the name plural. This caused problems because the component states were the same for every view of an object. When we switched from one view to another, some states weren't reset. This PR fixes this by: - Creating an effect at the same level of page change effect to set the `currentViewId` inside the object `contextStore` - Adding the `currentViewId` to the `recordIndexId` Follow ups: - We need to get rid of `packages/twenty-front/src/modules/views/states/currentViewIdComponentState.ts` and use the context store instead
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
const globalCoverage = {
|
const globalCoverage = {
|
||||||
branches: 24,
|
branches: 23,
|
||||||
statements: 40,
|
statements: 39,
|
||||||
lines: 40,
|
lines: 39,
|
||||||
functions: 30,
|
functions: 28,
|
||||||
exclude: ['src/generated/**/*'],
|
exclude: ['src/generated/**/*'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
import { getOperationName } from '@apollo/client/utilities';
|
|
||||||
import { jest } from '@storybook/jest';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { HttpResponse, graphql } from 'msw';
|
|
||||||
import { HelmetProvider } from 'react-helmet-async';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
|
||||||
|
|
||||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
|
||||||
import indexAppPath from '@/navigation/utils/indexAppPath';
|
|
||||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
|
||||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
|
||||||
|
|
||||||
import { AppRouter } from '@/app/components/AppRouter';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
|
||||||
import { IconsProvider } from 'twenty-ui';
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
|
||||||
import { mockedUserData } from '~/testing/mock-data/users';
|
|
||||||
|
|
||||||
const meta: Meta<typeof AppRouter> = {
|
|
||||||
title: 'App/AppRouter',
|
|
||||||
component: AppRouter,
|
|
||||||
decorators: [
|
|
||||||
(Story) => {
|
|
||||||
return (
|
|
||||||
<RecoilRoot>
|
|
||||||
<AppErrorBoundary>
|
|
||||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
|
||||||
<IconsProvider>
|
|
||||||
<HelmetProvider>
|
|
||||||
<Story />
|
|
||||||
</HelmetProvider>
|
|
||||||
</IconsProvider>
|
|
||||||
</SnackBarProviderScope>
|
|
||||||
</AppErrorBoundary>
|
|
||||||
</RecoilRoot>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
parameters: {
|
|
||||||
msw: graphqlMocks,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
export type Story = StoryObj<typeof AppRouter>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
play: async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(indexAppPath, 'getIndexAppPath')
|
|
||||||
.mockReturnValue('iframe.html' as AppPath);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DarkMode: Story = {
|
|
||||||
play: async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(indexAppPath, 'getIndexAppPath')
|
|
||||||
.mockReturnValue('iframe.html' as AppPath);
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
msw: {
|
|
||||||
handlers: [
|
|
||||||
...graphqlMocks.handlers.filter((handler) => {
|
|
||||||
return (handler.info as any).operationName !== 'GetCurrentUser';
|
|
||||||
}),
|
|
||||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
|
|
||||||
return HttpResponse.json({
|
|
||||||
data: {
|
|
||||||
currentUser: {
|
|
||||||
...mockedUserData,
|
|
||||||
workspaceMember: {
|
|
||||||
...mockedUserData.workspaceMember,
|
|
||||||
colorScheme: 'Dark',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -6,6 +6,7 @@ import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/compone
|
|||||||
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
||||||
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
||||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||||
|
import { ContextStoreViewIdEffect } from '@/context-store/components/ContextStoreViewIdEffect';
|
||||||
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
||||||
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
|
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
|
||||||
import { ObjectMetadataItemsGater } from '@/object-metadata/components/ObjectMetadataItemsGater';
|
import { ObjectMetadataItemsGater } from '@/object-metadata/components/ObjectMetadataItemsGater';
|
||||||
@ -21,13 +22,15 @@ import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
|||||||
import { UserProvider } from '@/users/components/UserProvider';
|
import { UserProvider } from '@/users/components/UserProvider';
|
||||||
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
||||||
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation, useParams } from 'react-router-dom';
|
||||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||||
|
|
||||||
export const AppRouterProviders = () => {
|
export const AppRouterProviders = () => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const pageTitle = getPageTitleFromPath(pathname);
|
const pageTitle = getPageTitleFromPath(pathname);
|
||||||
|
const objectNamePlural = useParams().objectNamePlural;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider>
|
<ApolloProvider>
|
||||||
@ -61,6 +64,11 @@ export const AppRouterProviders = () => {
|
|||||||
</PrefetchDataProvider>
|
</PrefetchDataProvider>
|
||||||
</ObjectMetadataItemsGater>
|
</ObjectMetadataItemsGater>
|
||||||
<PageChangeEffect />
|
<PageChangeEffect />
|
||||||
|
{isNonEmptyString(objectNamePlural) && (
|
||||||
|
<ContextStoreViewIdEffect
|
||||||
|
objectNamePlural={objectNamePlural}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ObjectMetadataItemsProvider>
|
</ObjectMetadataItemsProvider>
|
||||||
</ApolloMetadataClientProvider>
|
</ApolloMetadataClientProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@ -0,0 +1,104 @@
|
|||||||
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||||
|
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
|
||||||
|
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||||
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||||
|
import { View } from '@/views/types/View';
|
||||||
|
import { isUndefined } from '@sniptt/guards';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const ContextStoreViewIdEffect = ({
|
||||||
|
objectNamePlural,
|
||||||
|
}: {
|
||||||
|
objectNamePlural: string;
|
||||||
|
}) => {
|
||||||
|
const { viewIdQueryParam } = useViewFromQueryParams();
|
||||||
|
|
||||||
|
const { records: viewsOnCurrentObject } = usePrefetchedData<View>(
|
||||||
|
PrefetchKey.AllViews,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { findObjectMetadataItemByNamePlural } =
|
||||||
|
useFilteredObjectMetadataItems();
|
||||||
|
const objectMetadataItemId =
|
||||||
|
findObjectMetadataItemByNamePlural(objectNamePlural);
|
||||||
|
const { getLastVisitedViewIdFromObjectNamePlural, setLastVisitedView } =
|
||||||
|
useLastVisitedView();
|
||||||
|
const { lastVisitedObjectMetadataItemId, setLastVisitedObjectMetadataItem } =
|
||||||
|
useLastVisitedObjectMetadataItem();
|
||||||
|
|
||||||
|
const lastVisitedViewId =
|
||||||
|
getLastVisitedViewIdFromObjectNamePlural(objectNamePlural);
|
||||||
|
const isLastVisitedObjectMetadataItemDifferent = !isDeeplyEqual(
|
||||||
|
objectMetadataItemId?.id,
|
||||||
|
lastVisitedObjectMetadataItemId,
|
||||||
|
);
|
||||||
|
const setContextStoreCurrentViewId = useSetRecoilComponentStateV2(
|
||||||
|
contextStoreCurrentViewIdComponentState,
|
||||||
|
objectNamePlural,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX');
|
||||||
|
|
||||||
|
if (isUndefined(viewIdQueryParam) && isDefined(lastVisitedViewId)) {
|
||||||
|
if (isLastVisitedObjectMetadataItemDifferent) {
|
||||||
|
setLastVisitedObjectMetadataItem(objectNamePlural);
|
||||||
|
setLastVisitedView({
|
||||||
|
objectNamePlural,
|
||||||
|
viewId: lastVisitedViewId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setContextStoreCurrentViewId(lastVisitedViewId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(viewIdQueryParam)) {
|
||||||
|
if (isLastVisitedObjectMetadataItemDifferent) {
|
||||||
|
setLastVisitedObjectMetadataItem(objectNamePlural);
|
||||||
|
}
|
||||||
|
if (!isDeeplyEqual(viewIdQueryParam, lastVisitedViewId)) {
|
||||||
|
setLastVisitedView({
|
||||||
|
objectNamePlural,
|
||||||
|
viewId: viewIdQueryParam,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setContextStoreCurrentViewId(viewIdQueryParam);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(indexView)) {
|
||||||
|
if (isLastVisitedObjectMetadataItemDifferent) {
|
||||||
|
setLastVisitedObjectMetadataItem(objectNamePlural);
|
||||||
|
}
|
||||||
|
if (!isDeeplyEqual(indexView.id, lastVisitedViewId)) {
|
||||||
|
setLastVisitedView({
|
||||||
|
objectNamePlural,
|
||||||
|
viewId: indexView.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setContextStoreCurrentViewId(indexView.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setContextStoreCurrentViewId(null);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
isLastVisitedObjectMetadataItemDifferent,
|
||||||
|
lastVisitedViewId,
|
||||||
|
objectNamePlural,
|
||||||
|
setContextStoreCurrentViewId,
|
||||||
|
setLastVisitedObjectMetadataItem,
|
||||||
|
setLastVisitedView,
|
||||||
|
viewIdQueryParam,
|
||||||
|
viewsOnCurrentObject,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||||
import { MainContextStoreComponentInstanceIdSetterEffect } from '@/context-store/components/MainContextStoreComponentInstanceIdSetterEffect';
|
import { MainContextStoreComponentInstanceIdSetterEffect } from '@/context-store/components/MainContextStoreComponentInstanceIdSetterEffect';
|
||||||
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
@ -17,9 +18,11 @@ import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hook
|
|||||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||||
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize } from 'twenty-shared';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledIndexContainer = styled.div`
|
const StyledIndexContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -30,7 +33,12 @@ const StyledIndexContainer = styled.div`
|
|||||||
export const RecordIndexPage = () => {
|
export const RecordIndexPage = () => {
|
||||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||||
|
|
||||||
const recordIndexId = objectNamePlural ?? '';
|
const contextStoreCurrentViewId = useRecoilComponentValueV2(
|
||||||
|
contextStoreCurrentViewIdComponentState,
|
||||||
|
objectNamePlural,
|
||||||
|
);
|
||||||
|
|
||||||
|
const recordIndexId = `${objectNamePlural}-${contextStoreCurrentViewId}`;
|
||||||
|
|
||||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
@ -54,6 +62,10 @@ export const RecordIndexPage = () => {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isDefined(contextStoreCurrentViewId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<RecordIndexContextProvider
|
<RecordIndexContextProvider
|
||||||
|
|||||||
Reference in New Issue
Block a user