584 Refactor Tabs (#11008)
Closes https://github.com/twentyhq/core-team-issues/issues/584 This PR: - Migrates the component state `activeTabIdComponentState` from the deprecated V1 version to V2. - Allows the active tab state to be preserved during navigation inside the side panel and reset when the side panel is closed. - Allows the active tab state to be preserved when we open a record in full page from the side panel https://github.com/user-attachments/assets/f2329d7a-ea15-4bd8-81dc-e98ce11edbd0 https://github.com/user-attachments/assets/474bffd5-29e0-40ba-97f4-fa5e9be34dc2
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu';
|
||||
import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||
import { CardComponents } from '@/object-record/record-show/components/CardComponents';
|
||||
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
|
||||
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
|
||||
@ -9,9 +10,13 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
@ -41,8 +46,6 @@ const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
||||
isInRightDrawer ? theme.spacing(16) : 0};
|
||||
`;
|
||||
|
||||
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
|
||||
|
||||
type ShowPageSubContainerProps = {
|
||||
layout?: RecordLayout;
|
||||
tabs: SingleTabProps[];
|
||||
@ -52,7 +55,6 @@ type ShowPageSubContainerProps = {
|
||||
>;
|
||||
isInRightDrawer?: boolean;
|
||||
loading: boolean;
|
||||
isNewRightDrawerItemLoading?: boolean;
|
||||
};
|
||||
|
||||
export const ShowPageSubContainer = ({
|
||||
@ -62,9 +64,18 @@ export const ShowPageSubContainer = ({
|
||||
loading,
|
||||
isInRightDrawer = false,
|
||||
}: ShowPageSubContainerProps) => {
|
||||
const tabListComponentId = `${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}-${targetableObject.id}`;
|
||||
const commandMenuPageComponentInstance = useComponentInstanceStateContext(
|
||||
CommandMenuPageComponentInstanceContext,
|
||||
);
|
||||
|
||||
const { activeTabId } = useTabList(tabListComponentId);
|
||||
const tabListComponentId = getShowPageTabListComponentId({
|
||||
pageId: commandMenuPageComponentInstance?.instanceId,
|
||||
targetObjectId: targetableObject.id,
|
||||
});
|
||||
const activeTabId = useRecoilComponentValueV2(
|
||||
activeTabIdComponentState,
|
||||
tabListComponentId,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@ -109,7 +120,9 @@ export const ShowPageSubContainer = ({
|
||||
layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: tabListComponentId }}
|
||||
>
|
||||
{displaySummaryAndFields && (
|
||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||
{summaryCard}
|
||||
@ -121,9 +134,9 @@ export const ShowPageSubContainer = ({
|
||||
<StyledTabList
|
||||
behaveAsLinks={!isInRightDrawer}
|
||||
loading={loading}
|
||||
tabListInstanceId={tabListComponentId}
|
||||
tabs={tabs}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
componentInstanceId={tabListComponentId}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
{(isMobile || isInRightDrawer) && summaryCard}
|
||||
@ -142,6 +155,6 @@ export const ShowPageSubContainer = ({
|
||||
/>
|
||||
)}
|
||||
</StyledShowPageRightContainer>
|
||||
</>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const SHOW_PAGE_RIGHT_TAB_LIST = 'show-page-right-tab-list';
|
||||
@ -0,0 +1,12 @@
|
||||
import { SHOW_PAGE_RIGHT_TAB_LIST } from '@/ui/layout/show-page/constants/ShowPageTabListComponentId';
|
||||
|
||||
export const getShowPageTabListComponentId = ({
|
||||
pageId,
|
||||
targetObjectId,
|
||||
}: {
|
||||
pageId?: string;
|
||||
targetObjectId: string;
|
||||
}): string => {
|
||||
const id = pageId || targetObjectId;
|
||||
return `${SHOW_PAGE_RIGHT_TAB_LIST}-${id}`;
|
||||
};
|
||||
@ -1,8 +1,9 @@
|
||||
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
|
||||
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
@ -21,12 +22,12 @@ export type SingleTabProps<T extends string = string> = {
|
||||
};
|
||||
|
||||
type TabListProps = {
|
||||
tabListInstanceId: string;
|
||||
tabs: SingleTabProps[];
|
||||
loading?: boolean;
|
||||
behaveAsLinks?: boolean;
|
||||
className?: string;
|
||||
isInRightDrawer?: boolean;
|
||||
componentInstanceId: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -44,15 +45,18 @@ const StyledOuterContainer = styled.div`
|
||||
|
||||
export const TabList = ({
|
||||
tabs,
|
||||
tabListInstanceId,
|
||||
loading,
|
||||
behaveAsLinks = true,
|
||||
isInRightDrawer,
|
||||
className,
|
||||
componentInstanceId,
|
||||
}: TabListProps) => {
|
||||
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
||||
|
||||
const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId);
|
||||
const [activeTabId, setActiveTabId] = useRecoilComponentStateV2(
|
||||
activeTabIdComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const initialActiveTabId = activeTabId || visibleTabs[0]?.id || '';
|
||||
|
||||
@ -65,17 +69,18 @@ export const TabList = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledOuterContainer>
|
||||
<TabListScope tabListScopeId={tabListInstanceId}>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<StyledOuterContainer>
|
||||
<TabListFromUrlOptionalEffect
|
||||
isInRightDrawer={!!isInRightDrawer}
|
||||
componentInstanceId={tabListInstanceId}
|
||||
tabListIds={tabs.map((tab) => tab.id)}
|
||||
/>
|
||||
<ScrollWrapper
|
||||
defaultEnableYScroll={false}
|
||||
contextProviderName="tabList"
|
||||
componentInstanceId={`scroll-wrapper-tab-list-${tabListInstanceId}`}
|
||||
componentInstanceId={`scroll-wrapper-tab-list-${componentInstanceId}`}
|
||||
>
|
||||
<StyledContainer className={className}>
|
||||
{visibleTabs.map((tab) => (
|
||||
@ -98,7 +103,7 @@ export const TabList = ({
|
||||
))}
|
||||
</StyledContainer>
|
||||
</ScrollWrapper>
|
||||
</TabListScope>
|
||||
</StyledOuterContainer>
|
||||
</StyledOuterContainer>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
type TabListFromUrlOptionalEffectProps = {
|
||||
componentInstanceId: string;
|
||||
tabListIds: string[];
|
||||
isInRightDrawer: boolean;
|
||||
};
|
||||
|
||||
export const TabListFromUrlOptionalEffect = ({
|
||||
componentInstanceId,
|
||||
tabListIds,
|
||||
isInRightDrawer,
|
||||
}: TabListFromUrlOptionalEffectProps) => {
|
||||
const location = useLocation();
|
||||
const { activeTabId, setActiveTabId } = useTabList(componentInstanceId);
|
||||
const activeTabId = useRecoilComponentValueV2(activeTabIdComponentState);
|
||||
const setActiveTabId = useSetRecoilComponentStateV2(
|
||||
activeTabIdComponentState,
|
||||
);
|
||||
|
||||
const hash = location.hash.replace('#', '');
|
||||
|
||||
|
||||
@ -2,8 +2,6 @@ import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { ComponentWithRouterDecorator, IconCheckbox } from 'twenty-ui';
|
||||
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
import { TabList } from '../TabList';
|
||||
|
||||
const tabs = [
|
||||
@ -39,17 +37,10 @@ const meta: Meta<typeof TabList> = {
|
||||
title: 'UI/Layout/Tab/TabList',
|
||||
component: TabList,
|
||||
args: {
|
||||
tabListInstanceId: 'tab-list-id',
|
||||
tabs: tabs,
|
||||
componentInstanceId: 'tab-list',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<RecoilScope>
|
||||
<Story />
|
||||
</RecoilScope>
|
||||
),
|
||||
ComponentWithRouterDecorator,
|
||||
],
|
||||
decorators: [ComponentWithRouterDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useTabList } from '../useTabList';
|
||||
|
||||
describe('useTabList', () => {
|
||||
it('Should update the activeTabId state', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { activeTabId, setActiveTabId } = useTabList('TEST_TAB_LIST_ID');
|
||||
|
||||
return {
|
||||
activeTabId,
|
||||
setActiveTabId: setActiveTabId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
expect(result.current.setActiveTabId).toBeInstanceOf(Function);
|
||||
expect(result.current.activeTabId).toBeNull();
|
||||
|
||||
act(() => {
|
||||
result.current.setActiveTabId('test-value');
|
||||
});
|
||||
|
||||
expect(result.current.activeTabId).toBe('test-value');
|
||||
});
|
||||
});
|
||||
@ -1,20 +0,0 @@
|
||||
import { TabListScopeInternalContext } from '@/ui/layout/tab/scopes/scope-internal-context/TabListScopeInternalContext';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
|
||||
type useTabListStatesProps = {
|
||||
tabListScopeId?: string;
|
||||
};
|
||||
|
||||
export const useTabListStates = ({ tabListScopeId }: useTabListStatesProps) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
TabListScopeInternalContext,
|
||||
tabListScopeId,
|
||||
);
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
activeTabIdState: extractComponentState(activeTabIdComponentState, scopeId),
|
||||
};
|
||||
};
|
||||
@ -1,18 +0,0 @@
|
||||
import { RecoilState, useRecoilState } from 'recoil';
|
||||
|
||||
import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates';
|
||||
|
||||
export const useTabList = <T extends string>(tabListId?: string) => {
|
||||
const { activeTabIdState } = useTabListStates({
|
||||
tabListScopeId: tabListId,
|
||||
});
|
||||
|
||||
const [activeTabId, setActiveTabId] = useRecoilState(
|
||||
activeTabIdState as RecoilState<T | null>,
|
||||
);
|
||||
|
||||
return {
|
||||
activeTabId,
|
||||
setActiveTabId,
|
||||
};
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const activeTabIdComponentState = createComponentState<string | null>({
|
||||
export const activeTabIdComponentState = createComponentStateV2<string | null>({
|
||||
key: 'activeTabIdComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: TabListComponentInstanceContext,
|
||||
});
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const TabListComponentInstanceContext = createComponentInstanceContext();
|
||||
Reference in New Issue
Block a user