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:
Raphaël Bosi
2025-03-19 16:53:22 +01:00
committed by GitHub
parent 0d40126a29
commit cfdb3f5778
37 changed files with 299 additions and 609 deletions

View File

@ -1,15 +1,22 @@
import { CommandMenuActionMenuDropdownHotkeyScope } from '@/action-menu/types/CommandMenuActionMenuDropdownHotkeyScope'; import { CommandMenuActionMenuDropdownHotkeyScope } from '@/action-menu/types/CommandMenuActionMenuDropdownHotkeyScope';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage'; import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button, IconBrowserMaximize, getOsControlSymbol } from 'twenty-ui'; import { Button, IconBrowserMaximize, getOsControlSymbol } from 'twenty-ui';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
text-decoration: none; text-decoration: none;
`; `;
@ -25,17 +32,58 @@ export const RecordShowRightDrawerOpenRecordButton = ({
}: RecordShowRightDrawerOpenRecordButtonProps) => { }: RecordShowRightDrawerOpenRecordButtonProps) => {
const { closeCommandMenu } = useCommandMenu(); const { closeCommandMenu } = useCommandMenu();
const commandMenuPageComponentInstance = useComponentInstanceStateContext(
CommandMenuPageComponentInstanceContext,
);
const tabListComponentId = getShowPageTabListComponentId({
pageId: commandMenuPageComponentInstance?.instanceId,
targetObjectId: record.id,
});
const activeTabIdInRightDrawer = useRecoilComponentValueV2(
activeTabIdComponentState,
tabListComponentId,
);
const tabListComponentIdInRecordPage = getShowPageTabListComponentId({
targetObjectId: record.id,
});
const setActiveTabIdInRecordPage = useSetRecoilComponentStateV2(
activeTabIdComponentState,
tabListComponentIdInRecordPage,
);
const to = getLinkToShowPage(objectNameSingular, record); const to = getLinkToShowPage(objectNameSingular, record);
const navigate = useNavigateApp(); const navigate = useNavigateApp();
const handleOpenRecord = () => { const handleOpenRecord = useCallback(() => {
const tabIdToOpen =
activeTabIdInRightDrawer === 'home'
? objectNameSingular === CoreObjectNameSingular.Note ||
objectNameSingular === CoreObjectNameSingular.Task
? 'richText'
: 'timeline'
: activeTabIdInRightDrawer;
setActiveTabIdInRecordPage(tabIdToOpen);
navigate(AppPath.RecordShowPage, { navigate(AppPath.RecordShowPage, {
objectNameSingular, objectNameSingular,
objectRecordId: record.id, objectRecordId: record.id,
}); });
closeCommandMenu(); closeCommandMenu();
}; }, [
activeTabIdInRightDrawer,
closeCommandMenu,
navigate,
objectNameSingular,
record.id,
setActiveTabIdInRecordPage,
]);
useScopedHotkeys( useScopedHotkeys(
['ctrl+Enter,meta+Enter'], ['ctrl+Enter,meta+Enter'],

View File

@ -12,13 +12,13 @@ import {
import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { TASKS_TAB_LIST_COMPONENT_ID } from '@/activities/tasks/constants/TasksTabListComponentId';
import { useTasks } from '@/activities/tasks/hooks/useTasks'; import { useTasks } from '@/activities/tasks/hooks/useTasks';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Task } from '@/activities/types/Task'; import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
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 groupBy from 'lodash.groupby'; import groupBy from 'lodash.groupby';
import { AddTaskButton } from './AddTaskButton'; import { AddTaskButton } from './AddTaskButton';
import { TaskList } from './TaskList'; import { TaskList } from './TaskList';
@ -45,7 +45,7 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
activityObjectNameSingular: CoreObjectNameSingular.Task, activityObjectNameSingular: CoreObjectNameSingular.Task,
}); });
const { activeTabId } = useTabList(TASKS_TAB_LIST_COMPONENT_ID); const activeTabId = useRecoilComponentValueV2(activeTabIdComponentState);
const isLoading = const isLoading =
(activeTabId !== 'done' && tasksLoading) || (activeTabId !== 'done' && tasksLoading) ||

View File

@ -16,7 +16,13 @@ import { viewableRecordIdState } from '@/object-record/record-right-drawer/state
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/types/WorkflowServerlessFunctionTabId';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
export const useCommandMenuCloseAnimationCompleteCleanup = () => { export const useCommandMenuCloseAnimationCompleteCleanup = () => {
@ -29,7 +35,7 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
const { closeDropdown } = useDropdownV2(); const { closeDropdown } = useDropdownV2();
const commandMenuCloseAnimationCompleteCleanup = useRecoilCallback( const commandMenuCloseAnimationCompleteCleanup = useRecoilCallback(
({ set }) => ({ snapshot, set }) =>
() => { () => {
closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID); closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
@ -54,6 +60,32 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
emitRightDrawerCloseEvent(); emitRightDrawerCloseEvent();
set(isCommandMenuClosingState, false); set(isCommandMenuClosingState, false);
set(
activeTabIdComponentState.atomFamily({
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
}),
WorkflowRunTabId.NODE,
);
set(
activeTabIdComponentState.atomFamily({
instanceId: WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
}),
WorkflowServerlessFunctionTabId.CODE,
);
for (const [pageId, morphItem] of snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue()) {
set(
activeTabIdComponentState.atomFamily({
instanceId: getShowPageTabListComponentId({
pageId,
targetObjectId: morphItem.recordId,
}),
}),
null,
);
}
}, },
[ [
closeDropdown, closeDropdown,

View File

@ -6,6 +6,8 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
export const useCommandMenuHistory = () => { export const useCommandMenuHistory = () => {
@ -47,6 +49,19 @@ export const useCommandMenuHistory = () => {
const newMorphItems = new Map(currentMorphItems); const newMorphItems = new Map(currentMorphItems);
newMorphItems.delete(removedItem.pageId); newMorphItems.delete(removedItem.pageId);
set(commandMenuNavigationMorphItemByPageState, newMorphItems); set(commandMenuNavigationMorphItemByPageState, newMorphItems);
const morphItem = currentMorphItems.get(removedItem.pageId);
if (isDefined(morphItem)) {
set(
activeTabIdComponentState.atomFamily({
instanceId: getShowPageTabListComponentId({
pageId: removedItem.pageId,
targetObjectId: morphItem.recordId,
}),
}),
null,
);
}
} }
} }
@ -84,6 +99,20 @@ export const useCommandMenuHistory = () => {
.getLoadable(commandMenuNavigationMorphItemByPageState) .getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue(); .getValue();
for (const [pageId, morphItem] of currentMorphItems.entries()) {
if (!newNavigationStack.some((item) => item.pageId === pageId)) {
set(
activeTabIdComponentState.atomFamily({
instanceId: getShowPageTabListComponentId({
pageId,
targetObjectId: morphItem.recordId,
}),
}),
null,
);
}
}
const newMorphItems = new Map( const newMorphItems = new Map(
Array.from(currentMorphItems.entries()).filter(([pageId]) => Array.from(currentMorphItems.entries()).filter(([pageId]) =>
newNavigationStack.some((item) => item.pageId === pageId), newNavigationStack.some((item) => item.pageId === pageId),

View File

@ -1,5 +1,6 @@
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow'; import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
@ -9,6 +10,10 @@ import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail'; import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail'; import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId'; import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import {
WorkflowRunTabId,
WorkflowRunTabIdType,
} from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus'; import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -20,7 +25,7 @@ const StyledTabList = styled(TabList)`
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
`; `;
type TabId = 'node' | 'input' | 'output'; type TabId = WorkflowRunTabIdType;
export const CommandMenuWorkflowRunViewStep = () => { export const CommandMenuWorkflowRunViewStep = () => {
const flow = useFlowOrThrow(); const flow = useFlowOrThrow();
@ -29,7 +34,8 @@ export const CommandMenuWorkflowRunViewStep = () => {
const workflowRun = useWorkflowRun({ workflowRunId }); const workflowRun = useWorkflowRun({ workflowRunId });
const { activeTabId } = useTabList<TabId>( const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID, WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
); );
@ -46,15 +52,15 @@ export const CommandMenuWorkflowRunViewStep = () => {
stepExecutionStatus === 'not-executed'; stepExecutionStatus === 'not-executed';
const tabs: SingleTabProps<TabId>[] = [ const tabs: SingleTabProps<TabId>[] = [
{ id: 'node', title: 'Node', Icon: IconStepInto }, { id: WorkflowRunTabId.NODE, title: 'Node', Icon: IconStepInto },
{ {
id: 'input', id: WorkflowRunTabId.INPUT,
title: 'Input', title: 'Input',
Icon: IconLogin2, Icon: IconLogin2,
disabled: areInputAndOutputTabsDisabled, disabled: areInputAndOutputTabsDisabled,
}, },
{ {
id: 'output', id: WorkflowRunTabId.OUTPUT,
title: 'Output', title: 'Output',
Icon: IconLogout, Icon: IconLogout,
disabled: areInputAndOutputTabsDisabled, disabled: areInputAndOutputTabsDisabled,
@ -70,12 +76,12 @@ export const CommandMenuWorkflowRunViewStep = () => {
value={{ workflowVersionId: workflowRun.workflowVersionId }} value={{ workflowVersionId: workflowRun.workflowVersionId }}
> >
<StyledTabList <StyledTabList
tabListInstanceId={WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID}
tabs={tabs} tabs={tabs}
behaveAsLinks={false} behaveAsLinks={false}
componentInstanceId={WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID}
/> />
{activeTabId === 'node' ? ( {activeTabId === WorkflowRunTabId.NODE ? (
<WorkflowStepDetail <WorkflowStepDetail
readonly readonly
stepId={workflowSelectedNode} stepId={workflowSelectedNode}
@ -84,14 +90,14 @@ export const CommandMenuWorkflowRunViewStep = () => {
/> />
) : null} ) : null}
{activeTabId === 'input' ? ( {activeTabId === WorkflowRunTabId.INPUT ? (
<WorkflowRunStepInputDetail <WorkflowRunStepInputDetail
key={workflowSelectedNode} key={workflowSelectedNode}
stepId={workflowSelectedNode} stepId={workflowSelectedNode}
/> />
) : null} ) : null}
{activeTabId === 'output' ? ( {activeTabId === WorkflowRunTabId.OUTPUT ? (
<WorkflowRunStepOutputDetail <WorkflowRunStepOutputDetail
key={workflowSelectedNode} key={workflowSelectedNode}
stepId={workflowSelectedNode} stepId={workflowSelectedNode}

View File

@ -11,7 +11,8 @@ import { SettingsAccountsCalendarChannelsGeneral } from '@/settings/accounts/com
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId'; import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
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 React from 'react'; import React from 'react';
const StyledCalenderContainer = styled.div` const StyledCalenderContainer = styled.div`
@ -19,7 +20,8 @@ const StyledCalenderContainer = styled.div`
`; `;
export const SettingsAccountsCalendarChannelsContainer = () => { export const SettingsAccountsCalendarChannelsContainer = () => {
const { activeTabId } = useTabList( const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID, SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID,
); );
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
@ -63,10 +65,10 @@ export const SettingsAccountsCalendarChannelsContainer = () => {
{tabs.length > 1 && ( {tabs.length > 1 && (
<StyledCalenderContainer> <StyledCalenderContainer>
<TabList <TabList
tabListInstanceId={ tabs={tabs}
componentInstanceId={
SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID
} }
tabs={tabs}
/> />
</StyledCalenderContainer> </StyledCalenderContainer>
)} )}

View File

@ -10,7 +10,8 @@ import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/compo
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
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 React from 'react'; import React from 'react';
const StyledMessageContainer = styled.div` const StyledMessageContainer = styled.div`
@ -18,7 +19,8 @@ const StyledMessageContainer = styled.div`
`; `;
export const SettingsAccountsMessageChannelsContainer = () => { export const SettingsAccountsMessageChannelsContainer = () => {
const { activeTabId } = useTabList( const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID, SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
); );
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
@ -62,10 +64,10 @@ export const SettingsAccountsMessageChannelsContainer = () => {
{tabs.length > 1 && ( {tabs.length > 1 && (
<StyledMessageContainer> <StyledMessageContainer>
<TabList <TabList
tabListInstanceId={ tabs={tabs}
componentInstanceId={
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID
} }
tabs={tabs}
/> />
</StyledMessageContainer> </StyledMessageContainer>
)} )}

View File

@ -37,8 +37,8 @@ export const SettingsAdminContent = () => {
<> <>
<TabList <TabList
tabs={tabs} tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_TABS_ID}
behaveAsLinks={true} behaveAsLinks={true}
componentInstanceId={SETTINGS_ADMIN_TABS_ID}
/> />
<SettingsAdminTabContent /> <SettingsAdminTabContent />
</> </>

View File

@ -1,12 +1,10 @@
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent'; import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState'; import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
@ -29,6 +27,9 @@ import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer'; import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
import { SETTINGS_ADMIN_GENERAL_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminGeneralTabsId';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
@ -49,8 +50,9 @@ export const SettingsAdminGeneral = () => {
const [userIdentifier, setUserIdentifier] = useState(''); const [userIdentifier, setUserIdentifier] = useState('');
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { activeTabId, setActiveTabId } = useTabList( const [activeTabId, setActiveTabId] = useRecoilComponentStateV2(
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID, activeTabIdComponentState,
'settings-admin-general',
); );
const [userLookupResult, setUserLookupResult] = useRecoilState( const [userLookupResult, setUserLookupResult] = useRecoilState(
userLookupResultState, userLookupResultState,
@ -200,8 +202,8 @@ export const SettingsAdminGeneral = () => {
<StyledTabListContainer> <StyledTabListContainer>
<TabList <TabList
tabs={tabs} tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
behaveAsLinks={false} behaveAsLinks={false}
componentInstanceId={SETTINGS_ADMIN_GENERAL_TABS_ID}
/> />
</StyledTabListContainer> </StyledTabListContainer>

View File

@ -3,10 +3,14 @@ import { SettingsAdminGeneral } from '@/settings/admin-panel/components/Settings
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs'; import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId'; import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus'; import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
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';
export const SettingsAdminTabContent = () => { export const SettingsAdminTabContent = () => {
const { activeTabId } = useTabList(SETTINGS_ADMIN_TABS_ID); const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
SETTINGS_ADMIN_TABS_ID,
);
switch (activeTabId) { switch (activeTabId) {
case SETTINGS_ADMIN_TABS.GENERAL: case SETTINGS_ADMIN_TABS.GENERAL:

View File

@ -0,0 +1 @@
export const SETTINGS_ADMIN_GENERAL_TABS_ID = 'settings-admin-general-tabs-id';

View File

@ -6,8 +6,9 @@ import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/s
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { import {
@ -45,7 +46,8 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
onChange: (filePath: string, value: string) => void; onChange: (filePath: string, value: string) => void;
setIsCodeValid: (isCodeValid: boolean) => void; setIsCodeValid: (isCodeValid: boolean) => void;
}) => { }) => {
const { activeTabId } = useTabList( const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
); );
const TestButton = ( const TestButton = (
@ -81,12 +83,12 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
const HeaderTabList = ( const HeaderTabList = (
<StyledTabList <StyledTabList
tabListInstanceId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
tabs={files tabs={files
.filter((file) => file.path !== '.env') .filter((file) => file.path !== '.env')
.map((file) => { .map((file) => {
return { id: file.path, title: file.path.split('/').at(-1) || '' }; return { id: file.path, title: file.path.split('/').at(-1) || '' };
})} })}
componentInstanceId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
/> />
); );

View File

@ -1,6 +1,7 @@
import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu'; import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu';
import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton'; import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
import { CardComponents } from '@/object-record/record-show/components/CardComponents'; import { CardComponents } from '@/object-record/record-show/components/CardComponents';
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard'; import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard'; 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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter'; import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer'; 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 { 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 { 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 styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
@ -41,8 +46,6 @@ const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
isInRightDrawer ? theme.spacing(16) : 0}; isInRightDrawer ? theme.spacing(16) : 0};
`; `;
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
type ShowPageSubContainerProps = { type ShowPageSubContainerProps = {
layout?: RecordLayout; layout?: RecordLayout;
tabs: SingleTabProps[]; tabs: SingleTabProps[];
@ -52,7 +55,6 @@ type ShowPageSubContainerProps = {
>; >;
isInRightDrawer?: boolean; isInRightDrawer?: boolean;
loading: boolean; loading: boolean;
isNewRightDrawerItemLoading?: boolean;
}; };
export const ShowPageSubContainer = ({ export const ShowPageSubContainer = ({
@ -62,9 +64,18 @@ export const ShowPageSubContainer = ({
loading, loading,
isInRightDrawer = false, isInRightDrawer = false,
}: ShowPageSubContainerProps) => { }: 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(); const isMobile = useIsMobile();
@ -109,7 +120,9 @@ export const ShowPageSubContainer = ({
layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer; layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer;
return ( return (
<> <TabListComponentInstanceContext.Provider
value={{ instanceId: tabListComponentId }}
>
{displaySummaryAndFields && ( {displaySummaryAndFields && (
<ShowPageLeftContainer forceMobile={isMobile}> <ShowPageLeftContainer forceMobile={isMobile}>
{summaryCard} {summaryCard}
@ -121,9 +134,9 @@ export const ShowPageSubContainer = ({
<StyledTabList <StyledTabList
behaveAsLinks={!isInRightDrawer} behaveAsLinks={!isInRightDrawer}
loading={loading} loading={loading}
tabListInstanceId={tabListComponentId}
tabs={tabs} tabs={tabs}
isInRightDrawer={isInRightDrawer} isInRightDrawer={isInRightDrawer}
componentInstanceId={tabListComponentId}
/> />
</StyledTabListContainer> </StyledTabListContainer>
{(isMobile || isInRightDrawer) && summaryCard} {(isMobile || isInRightDrawer) && summaryCard}
@ -142,6 +155,6 @@ export const ShowPageSubContainer = ({
/> />
)} )}
</StyledShowPageRightContainer> </StyledShowPageRightContainer>
</> </TabListComponentInstanceContext.Provider>
); );
}; };

View File

@ -0,0 +1 @@
export const SHOW_PAGE_RIGHT_TAB_LIST = 'show-page-right-tab-list';

View File

@ -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}`;
};

View File

@ -1,8 +1,9 @@
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect'; import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope'; import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard'; import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import * as React from 'react'; import * as React from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
@ -21,12 +22,12 @@ export type SingleTabProps<T extends string = string> = {
}; };
type TabListProps = { type TabListProps = {
tabListInstanceId: string;
tabs: SingleTabProps[]; tabs: SingleTabProps[];
loading?: boolean; loading?: boolean;
behaveAsLinks?: boolean; behaveAsLinks?: boolean;
className?: string; className?: string;
isInRightDrawer?: boolean; isInRightDrawer?: boolean;
componentInstanceId: string;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -44,15 +45,18 @@ const StyledOuterContainer = styled.div`
export const TabList = ({ export const TabList = ({
tabs, tabs,
tabListInstanceId,
loading, loading,
behaveAsLinks = true, behaveAsLinks = true,
isInRightDrawer, isInRightDrawer,
className, className,
componentInstanceId,
}: TabListProps) => { }: TabListProps) => {
const visibleTabs = tabs.filter((tab) => !tab.hide); const visibleTabs = tabs.filter((tab) => !tab.hide);
const { activeTabId, setActiveTabId } = useTabList(tabListInstanceId); const [activeTabId, setActiveTabId] = useRecoilComponentStateV2(
activeTabIdComponentState,
componentInstanceId,
);
const initialActiveTabId = activeTabId || visibleTabs[0]?.id || ''; const initialActiveTabId = activeTabId || visibleTabs[0]?.id || '';
@ -65,17 +69,18 @@ export const TabList = ({
} }
return ( return (
<StyledOuterContainer> <TabListComponentInstanceContext.Provider
<TabListScope tabListScopeId={tabListInstanceId}> value={{ instanceId: componentInstanceId }}
>
<StyledOuterContainer>
<TabListFromUrlOptionalEffect <TabListFromUrlOptionalEffect
isInRightDrawer={!!isInRightDrawer} isInRightDrawer={!!isInRightDrawer}
componentInstanceId={tabListInstanceId}
tabListIds={tabs.map((tab) => tab.id)} tabListIds={tabs.map((tab) => tab.id)}
/> />
<ScrollWrapper <ScrollWrapper
defaultEnableYScroll={false} defaultEnableYScroll={false}
contextProviderName="tabList" contextProviderName="tabList"
componentInstanceId={`scroll-wrapper-tab-list-${tabListInstanceId}`} componentInstanceId={`scroll-wrapper-tab-list-${componentInstanceId}`}
> >
<StyledContainer className={className}> <StyledContainer className={className}>
{visibleTabs.map((tab) => ( {visibleTabs.map((tab) => (
@ -98,7 +103,7 @@ export const TabList = ({
))} ))}
</StyledContainer> </StyledContainer>
</ScrollWrapper> </ScrollWrapper>
</TabListScope> </StyledOuterContainer>
</StyledOuterContainer> </TabListComponentInstanceContext.Provider>
); );
}; };

View File

@ -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 { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
type TabListFromUrlOptionalEffectProps = { type TabListFromUrlOptionalEffectProps = {
componentInstanceId: string;
tabListIds: string[]; tabListIds: string[];
isInRightDrawer: boolean; isInRightDrawer: boolean;
}; };
export const TabListFromUrlOptionalEffect = ({ export const TabListFromUrlOptionalEffect = ({
componentInstanceId,
tabListIds, tabListIds,
isInRightDrawer, isInRightDrawer,
}: TabListFromUrlOptionalEffectProps) => { }: TabListFromUrlOptionalEffectProps) => {
const location = useLocation(); const location = useLocation();
const { activeTabId, setActiveTabId } = useTabList(componentInstanceId); const activeTabId = useRecoilComponentValueV2(activeTabIdComponentState);
const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState,
);
const hash = location.hash.replace('#', ''); const hash = location.hash.replace('#', '');

View File

@ -2,8 +2,6 @@ import { Meta, StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test'; import { expect, within } from '@storybook/test';
import { ComponentWithRouterDecorator, IconCheckbox } from 'twenty-ui'; import { ComponentWithRouterDecorator, IconCheckbox } from 'twenty-ui';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { TabList } from '../TabList'; import { TabList } from '../TabList';
const tabs = [ const tabs = [
@ -39,17 +37,10 @@ const meta: Meta<typeof TabList> = {
title: 'UI/Layout/Tab/TabList', title: 'UI/Layout/Tab/TabList',
component: TabList, component: TabList,
args: { args: {
tabListInstanceId: 'tab-list-id',
tabs: tabs, tabs: tabs,
componentInstanceId: 'tab-list',
}, },
decorators: [ decorators: [ComponentWithRouterDecorator],
(Story) => (
<RecoilScope>
<Story />
</RecoilScope>
),
ComponentWithRouterDecorator,
],
}; };
export default meta; export default meta;

View File

@ -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');
});
});

View File

@ -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),
};
};

View File

@ -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,
};
};

View File

@ -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', key: 'activeTabIdComponentState',
defaultValue: null, defaultValue: null,
componentInstanceContext: TabListComponentInstanceContext,
}); });

View File

@ -0,0 +1,3 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const TabListComponentInstanceContext = createComponentInstanceContext();

View File

@ -1,5 +1,5 @@
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { workflowIdState } from '@/workflow/states/workflowIdState'; import { workflowIdState } from '@/workflow/states/workflowIdState';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
@ -24,27 +24,29 @@ export const WorkflowRunDiagramCanvasEffect = () => {
const workflowId = useRecoilValue(workflowIdState); const workflowId = useRecoilValue(workflowIdState);
const { activeTabIdState: workflowRunRightDrawerListActiveTabIdState } =
useTabListStates({
tabListScopeId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
});
const goBackToFirstWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback( const goBackToFirstWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
() => { () => {
const activeWorkflowRunRightDrawerTab = getSnapshotValue( const activeWorkflowRunRightDrawerTab = getSnapshotValue(
snapshot, snapshot,
workflowRunRightDrawerListActiveTabIdState, activeTabIdComponentState.atomFamily({
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
}),
) as WorkflowRunTabId | null; ) as WorkflowRunTabId | null;
if ( if (
activeWorkflowRunRightDrawerTab === 'input' || activeWorkflowRunRightDrawerTab === 'input' ||
activeWorkflowRunRightDrawerTab === 'output' activeWorkflowRunRightDrawerTab === 'output'
) { ) {
set(workflowRunRightDrawerListActiveTabIdState, 'node'); set(
activeTabIdComponentState.atomFamily({
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
}),
'node',
);
} }
}, },
[workflowRunRightDrawerListActiveTabIdState], [],
); );
const handleSelectionChange = useCallback( const handleSelectionChange = useCallback(

View File

@ -1,23 +0,0 @@
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { WorkflowStepContextProvider } from '@/workflow/states/context/WorkflowStepContext';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { RightDrawerWorkflowEditStepContent } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
export const RightDrawerWorkflowEditStep = () => {
const workflowId = useRecoilValue(workflowIdState);
const workflow = useWorkflowWithCurrentVersion(workflowId);
if (!isDefined(workflow)) {
return null;
}
return (
<WorkflowStepContextProvider
value={{ workflowVersionId: workflow.currentVersion.id }}
>
<RightDrawerWorkflowEditStepContent workflow={workflow} />
</WorkflowStepContextProvider>
);
};

View File

@ -1,30 +0,0 @@
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
export const RightDrawerWorkflowEditStepContent = ({
workflow,
}: {
workflow: WorkflowWithCurrentVersion;
}) => {
const flow = useFlowOrThrow();
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
const { updateStep } = useUpdateStep({
workflow,
});
return (
<WorkflowStepDetail
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}
onActionUpdate={updateStep}
onTriggerUpdate={updateTrigger}
/>
);
};

View File

@ -1,102 +0,0 @@
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { WorkflowStepContextProvider } from '@/workflow/states/context/WorkflowStepContext';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared';
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
const StyledTabList = styled(TabList)`
background-color: ${({ theme }) => theme.background.secondary};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
type TabId = 'node' | 'input' | 'output';
export const RightDrawerWorkflowRunViewStep = () => {
const flow = useFlowOrThrow();
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
const workflowRunId = useWorkflowRunIdOrThrow();
const workflowRun = useWorkflowRun({ workflowRunId });
const { activeTabId } = useTabList<TabId>(
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
);
const stepExecutionStatus = isDefined(workflowRun)
? getWorkflowRunStepExecutionStatus({
workflowRunOutput: workflowRun.output,
stepId: workflowSelectedNode,
})
: undefined;
const areInputAndOutputTabsDisabled =
workflowSelectedNode === TRIGGER_STEP_ID ||
stepExecutionStatus === 'running' ||
stepExecutionStatus === 'not-executed';
const tabs: SingleTabProps<TabId>[] = [
{ id: 'node', title: 'Node', Icon: IconStepInto },
{
id: 'input',
title: 'Input',
Icon: IconLogin2,
disabled: areInputAndOutputTabsDisabled,
},
{
id: 'output',
title: 'Output',
Icon: IconLogout,
disabled: areInputAndOutputTabsDisabled,
},
];
if (!isDefined(workflowRun)) {
return null;
}
return (
<WorkflowStepContextProvider
value={{ workflowVersionId: flow.workflowVersionId }}
>
<StyledTabList
tabListInstanceId={WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID}
tabs={tabs}
behaveAsLinks={false}
/>
{activeTabId === 'node' ? (
<WorkflowStepDetail
readonly
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}
/>
) : null}
{activeTabId === 'input' ? (
<WorkflowRunStepInputDetail
key={workflowSelectedNode}
stepId={workflowSelectedNode}
/>
) : null}
{activeTabId === 'output' ? (
<WorkflowRunStepOutputDetail
key={workflowSelectedNode}
stepId={workflowSelectedNode}
/>
) : null}
</WorkflowStepContextProvider>
);
};

View File

@ -1,213 +0,0 @@
import { flowState } from '@/workflow/states/flowState';
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import styled from '@emotion/styled';
import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import { HttpResponse, graphql } from 'msw';
import { useSetRecoilState } from 'recoil';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { oneFailedWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
import { RightDrawerWorkflowRunViewStep } from '../RightDrawerWorkflowRunViewStep';
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
width: 500px;
`;
const meta: Meta<typeof RightDrawerWorkflowRunViewStep> = {
title: 'Modules/Workflow/RightDrawerWorkflowRunViewStep',
component: RightDrawerWorkflowRunViewStep,
decorators: [
(Story) => (
<StyledWrapper>
<Story />
</StyledWrapper>
),
I18nFrontDecorator,
ComponentDecorator,
(Story) => {
const setFlow = useSetRecoilState(flowState);
const setWorkflowSelectedNode = useSetRecoilState(
workflowSelectedNodeState,
);
const setWorkflowRunId = useSetRecoilState(workflowRunIdState);
setFlow({
workflowVersionId:
oneFailedWorkflowRunQueryResult.workflowRun.workflowVersionId,
trigger:
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.trigger,
steps: oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps,
});
setWorkflowSelectedNode(
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps[0].id,
);
setWorkflowRunId(oneFailedWorkflowRunQueryResult.workflowRun.id);
return <Story />;
},
RouterDecorator,
ObjectMetadataItemsDecorator,
WorkspaceDecorator,
WorkflowStepDecorator,
],
parameters: {
msw: {
handlers: [
graphql.query('FindOneWorkflowRun', () => {
const workflowRunContext =
oneFailedWorkflowRunQueryResult.workflowRun.context;
// Rendering the whole objectMetadata information in the JSON viewer is too long for storybook
// so we remove it for the story
return HttpResponse.json({
data: {
...oneFailedWorkflowRunQueryResult,
workflowRun: {
...oneFailedWorkflowRunQueryResult.workflowRun,
context: {
...workflowRunContext,
trigger: {
...workflowRunContext.trigger,
objectMetadata: undefined,
},
},
},
},
});
}),
...graphqlMocks.handlers,
],
},
},
};
export default meta;
type Story = StoryObj<typeof RightDrawerWorkflowRunViewStep>;
export const NodeTab: Story = {};
export const InputTab: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(await canvas.findByRole('button', { name: 'Input' }));
expect(await canvas.findByText('Trigger')).toBeVisible();
},
};
export const InputTabDisabledForTrigger: Story = {
decorators: [
(Story) => {
const setWorkflowSelectedNode = useSetRecoilState(
workflowSelectedNodeState,
);
setWorkflowSelectedNode(TRIGGER_STEP_ID);
return <Story />;
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const inputTab = await canvas.findByRole('button', { name: 'Input' });
expect(inputTab).toBeDisabled();
},
};
export const InputTabNotExecutedStep: Story = {
decorators: [
(Story) => {
const setWorkflowSelectedNode = useSetRecoilState(
workflowSelectedNodeState,
);
setWorkflowSelectedNode(
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps.at(-1)!
.id,
);
return <Story />;
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const inputTab = await canvas.findByRole('button', { name: 'Input' });
expect(inputTab).toBeDisabled();
},
};
export const OutputTab: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(
await canvas.findByRole('button', { name: 'Output' }),
);
await waitFor(() => {
expect(canvas.queryByText('Create Record')).not.toBeInTheDocument();
});
expect(await canvas.findByText('result')).toBeVisible();
},
};
export const OutputTabDisabledForTrigger: Story = {
decorators: [
(Story) => {
const setWorkflowSelectedNode = useSetRecoilState(
workflowSelectedNodeState,
);
setWorkflowSelectedNode(TRIGGER_STEP_ID);
return <Story />;
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const outputTab = await canvas.findByRole('button', { name: 'Output' });
expect(outputTab).toBeDisabled();
},
};
export const OutputTabNotExecutedStep: Story = {
decorators: [
(Story) => {
const setWorkflowSelectedNode = useSetRecoilState(
workflowSelectedNodeState,
);
setWorkflowSelectedNode(
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps.at(-1)!
.id,
);
return <Story />;
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const outputTab = await canvas.findByRole('button', { name: 'Output' });
expect(outputTab).toBeDisabled();
},
};

View File

@ -1 +1,7 @@
export type WorkflowRunTabId = 'node' | 'input' | 'output'; export type WorkflowRunTabIdType = 'node' | 'input' | 'output';
export enum WorkflowRunTabId {
NODE = 'node',
INPUT = 'input',
OUTPUT = 'output',
}

View File

@ -1,16 +0,0 @@
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { RightDrawerWorkflowSelectActionContent } from '@/workflow/workflow-steps/workflow-actions/components/RightDrawerWorkflowSelectActionContent';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
export const RightDrawerWorkflowSelectAction = () => {
const workflowId = useRecoilValue(workflowIdState);
const workflow = useWorkflowWithCurrentVersion(workflowId);
if (!isDefined(workflow)) {
return null;
}
return <RightDrawerWorkflowSelectActionContent workflow={workflow} />;
};

View File

@ -1,45 +0,0 @@
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
import { MenuItemCommand, useIcons } from 'twenty-ui';
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
export const RightDrawerWorkflowSelectActionContent = ({
workflow,
}: {
workflow: WorkflowWithCurrentVersion;
}) => {
const { getIcon } = useIcons();
const { createStep } = useCreateStep({
workflow,
});
return (
<RightDrawerStepListContainer>
<RightDrawerWorkflowSelectStepTitle>
Records
</RightDrawerWorkflowSelectStepTitle>
{RECORD_ACTIONS.map((action) => (
<MenuItemCommand
key={action.type}
LeftIcon={getIcon(action.icon)}
text={action.label}
onClick={() => createStep(action.type)}
/>
))}
<RightDrawerWorkflowSelectStepTitle>
Other
</RightDrawerWorkflowSelectStepTitle>
{OTHER_ACTIONS.map((action) => (
<MenuItemCommand
key={action.type}
LeftIcon={getIcon(action.icon)}
text={action.label}
onClick={() => createStep(action.type)}
/>
))}
</RightDrawerStepListContainer>
);
};

View File

@ -19,11 +19,13 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
import { TextArea } from '@/ui/input/components/TextArea'; import { TextArea } from '@/ui/input/components/TextArea';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter'; import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
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 { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState'; import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunctionFields'; import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunctionFields';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId'; import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/types/WorkflowServerlessFunctionTabId';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers'; import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
@ -77,8 +79,10 @@ export const WorkflowEditActionServerlessFunction = ({
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const serverlessFunctionId = action.settings.input.serverlessFunctionId; const serverlessFunctionId = action.settings.input.serverlessFunctionId;
const tabListId = `${WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}_${serverlessFunctionId}`; const activeTabId = useRecoilComponentValueV2(
const { activeTabId } = useTabList(tabListId); activeTabIdComponentState,
WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
);
const { updateOneServerlessFunction } = const { updateOneServerlessFunction } =
useUpdateOneServerlessFunction(serverlessFunctionId); useUpdateOneServerlessFunction(serverlessFunctionId);
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
@ -267,8 +271,12 @@ export const WorkflowEditActionServerlessFunction = ({
}; };
const tabs = [ const tabs = [
{ id: 'code', title: 'Code', Icon: IconCode }, { id: WorkflowServerlessFunctionTabId.CODE, title: 'Code', Icon: IconCode },
{ id: 'test', title: 'Test', Icon: IconPlayerPlay }, {
id: WorkflowServerlessFunctionTabId.TEST,
title: 'Test',
Icon: IconPlayerPlay,
},
]; ];
useEffect(() => { useEffect(() => {
@ -284,9 +292,11 @@ export const WorkflowEditActionServerlessFunction = ({
!loading && ( !loading && (
<StyledContainer> <StyledContainer>
<StyledTabList <StyledTabList
tabListInstanceId={tabListId}
tabs={tabs} tabs={tabs}
behaveAsLinks={false} behaveAsLinks={false}
componentInstanceId={
WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID
}
/> />
<WorkflowStepHeader <WorkflowStepHeader
onTitleChange={(newName: string) => { onTitleChange={(newName: string) => {
@ -299,7 +309,7 @@ export const WorkflowEditActionServerlessFunction = ({
disabled={actionOptions.readonly} disabled={actionOptions.readonly}
/> />
<WorkflowStepBody> <WorkflowStepBody>
{activeTabId === 'code' && ( {activeTabId === WorkflowServerlessFunctionTabId.CODE && (
<> <>
<WorkflowEditActionServerlessFunctionFields <WorkflowEditActionServerlessFunctionFields
functionInput={functionInput} functionInput={functionInput}
@ -323,7 +333,7 @@ export const WorkflowEditActionServerlessFunction = ({
</StyledCodeEditorContainer> </StyledCodeEditorContainer>
</> </>
)} )}
{activeTabId === 'test' && ( {activeTabId === WorkflowServerlessFunctionTabId.TEST && (
<> <>
<WorkflowEditActionServerlessFunctionFields <WorkflowEditActionServerlessFunctionFields
functionInput={serverlessFunctionTestData.input} functionInput={serverlessFunctionTestData.input}
@ -352,7 +362,7 @@ export const WorkflowEditActionServerlessFunction = ({
</> </>
)} )}
</WorkflowStepBody> </WorkflowStepBody>
{activeTabId === 'test' && ( {activeTabId === WorkflowServerlessFunctionTabId.TEST && (
<RightDrawerFooter <RightDrawerFooter
actions={[ actions={[
<CmdEnterActionButton <CmdEnterActionButton

View File

@ -0,0 +1,6 @@
export type WorkflowServerlessFunctionTabIdType = 'code' | 'test';
export enum WorkflowServerlessFunctionTabId {
CODE = 'code',
TEST = 'test',
}

View File

@ -10,10 +10,11 @@ import {
import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema'; import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema';
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema'; import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionState'; import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionState';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId';
import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath'; import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath';
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel'; import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
import { isLinkOutputSchema } from '@/workflow/workflow-variables/utils/isLinkOutputSchema'; import { isLinkOutputSchema } from '@/workflow/workflow-variables/utils/isLinkOutputSchema';
@ -26,7 +27,6 @@ import {
OverflowingTextWithTooltip, OverflowingTextWithTooltip,
useIcons, useIcons,
} from 'twenty-ui'; } from 'twenty-ui';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
type WorkflowVariablesDropdownFieldItemsProps = { type WorkflowVariablesDropdownFieldItemsProps = {
step: StepOutputSchema; step: StepOutputSchema;
@ -43,8 +43,8 @@ export const WorkflowVariablesDropdownFieldItems = ({
const [searchInputValue, setSearchInputValue] = useState(''); const [searchInputValue, setSearchInputValue] = useState('');
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
const { setActiveTabId } = useTabList( const setActiveTabId = useSetRecoilComponentStateV2(
WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, activeTabIdComponentState,
); );
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState( const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
workflowDiagramTriggerNodeSelectionState, workflowDiagramTriggerNodeSelectionState,

View File

@ -12,12 +12,13 @@ import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { import {
@ -68,7 +69,8 @@ export const SettingsObjectDetailPage = () => {
findActiveObjectMetadataItemByNamePlural(objectNamePlural) ?? findActiveObjectMetadataItemByNamePlural(objectNamePlural) ??
findActiveObjectMetadataItemByNamePlural(updatedObjectNamePlural); findActiveObjectMetadataItemByNamePlural(updatedObjectNamePlural);
const { activeTabId } = useTabList( const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState,
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID, SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID,
); );
@ -169,11 +171,10 @@ export const SettingsObjectDetailPage = () => {
> >
<SettingsPageContainer> <SettingsPageContainer>
<TabList <TabList
tabListInstanceId={ tabs={tabs}
componentInstanceId={
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID
} }
tabs={tabs}
className="tab-list"
/> />
<StyledContentContainer> <StyledContentContainer>
{renderActiveTabContent()} {renderActiveTabContent()}

View File

@ -10,7 +10,8 @@ import { RoleSettings } from '@/settings/roles/role-settings/components/RoleSett
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
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 { useGetRolesQuery } from '~/generated/graphql'; import { useGetRolesQuery } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
@ -33,9 +34,7 @@ export const SettingsRoleEdit = () => {
const role = rolesData?.getRoles.find((r) => r.id === roleId); const role = rolesData?.getRoles.find((r) => r.id === roleId);
const { activeTabId } = useTabList( const activeTabId = useRecoilComponentValueV2(activeTabIdComponentState);
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID,
);
useEffect(() => { useEffect(() => {
if (!rolesLoading && !role) { if (!rolesLoading && !role) {
@ -101,9 +100,11 @@ export const SettingsRoleEdit = () => {
{!rolesLoading && role ? ( {!rolesLoading && role ? (
<SettingsPageContainer> <SettingsPageContainer>
<TabList <TabList
tabListInstanceId={SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID}
tabs={tabs} tabs={tabs}
className="tab-list" className="tab-list"
componentInstanceId={
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID
}
/> />
{renderActiveTabContent()} {renderActiveTabContent()}
</SettingsPageContainer> </SettingsPageContainer>

View File

@ -14,7 +14,8 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useState } from 'react'; import { useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -26,12 +27,15 @@ import { FeatureFlagKey } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail'; const SERVERLESS_FUNCTION_DETAIL_ID = 'serverless-function-detail';
export const SettingsServerlessFunctionDetail = () => { export const SettingsServerlessFunctionDetail = () => {
const { serverlessFunctionId = '' } = useParams(); const { serverlessFunctionId = '' } = useParams();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { activeTabId, setActiveTabId } = useTabList(TAB_LIST_COMPONENT_ID); const [activeTabId, setActiveTabId] = useRecoilComponentStateV2(
activeTabIdComponentState,
SERVERLESS_FUNCTION_DETAIL_ID,
);
const [isCodeValid, setIsCodeValid] = useState(true); const [isCodeValid, setIsCodeValid] = useState(true);
const { updateOneServerlessFunction } = const { updateOneServerlessFunction } =
useUpdateOneServerlessFunction(serverlessFunctionId); useUpdateOneServerlessFunction(serverlessFunctionId);
@ -209,9 +213,9 @@ export const SettingsServerlessFunctionDetail = () => {
> >
<SettingsPageContainer> <SettingsPageContainer>
<TabList <TabList
tabListInstanceId={TAB_LIST_COMPONENT_ID}
tabs={tabs} tabs={tabs}
behaveAsLinks={false} behaveAsLinks={false}
componentInstanceId={SERVERLESS_FUNCTION_DETAIL_ID}
/> />
{renderActiveTabContent()} {renderActiveTabContent()}
</SettingsPageContainer> </SettingsPageContainer>