feat: new tab list (#12384)
closes #9904 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -3,31 +3,25 @@ import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-men
|
|||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
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 { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||||
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconBrowserMaximize } from 'twenty-ui/display';
|
import { IconBrowserMaximize } from 'twenty-ui/display';
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
const StyledLink = styled(Link)`
|
|
||||||
text-decoration: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type RecordShowRightDrawerOpenRecordButtonProps = {
|
type RecordShowRightDrawerOpenRecordButtonProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
@ -117,18 +111,15 @@ export const RecordShowRightDrawerOpenRecordButton = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const to = getLinkToShowPage(objectNameSingular, record);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLink to={to} onClick={closeCommandMenu}>
|
<Button
|
||||||
<Button
|
title="Open"
|
||||||
title="Open"
|
variant="primary"
|
||||||
variant="primary"
|
accent="blue"
|
||||||
accent="blue"
|
size="medium"
|
||||||
size="medium"
|
Icon={IconBrowserMaximize}
|
||||||
Icon={IconBrowserMaximize}
|
hotkeys={[getOsControlSymbol(), '⏎']}
|
||||||
hotkeys={[getOsControlSymbol(), '⏎']}
|
onClick={handleOpenRecord}
|
||||||
/>
|
/>
|
||||||
</StyledLink>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,8 +7,7 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
|
|||||||
import { Task } from '@/activities/types/Task';
|
import { Task } from '@/activities/types/Task';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import groupBy from 'lodash.groupby';
|
import groupBy from 'lodash.groupby';
|
||||||
import { IconPlus } from 'twenty-ui/display';
|
import { IconPlus } from 'twenty-ui/display';
|
||||||
@ -23,6 +22,7 @@ import {
|
|||||||
} from 'twenty-ui/layout';
|
} from 'twenty-ui/layout';
|
||||||
import { AddTaskButton } from './AddTaskButton';
|
import { AddTaskButton } from './AddTaskButton';
|
||||||
import { TaskList } from './TaskList';
|
import { TaskList } from './TaskList';
|
||||||
|
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
|
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
|||||||
@ -17,7 +17,7 @@ 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 { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
|
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
|
||||||
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
|
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageI
|
|||||||
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 { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const useCommandMenuHistory = () => {
|
export const useCommandMenuHistory = () => {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
||||||
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
||||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
|
import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||||
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { BASE_RECORD_LAYOUT } from '@/object-record/record-show/constants/BaseRecordLayout';
|
import { BASE_RECORD_LAYOUT } from '@/object-record/record-show/constants/BaseRecordLayout';
|
||||||
import { CardType } from '@/object-record/record-show/types/CardType';
|
import { CardType } from '@/object-record/record-show/types/CardType';
|
||||||
import { RecordLayout } from '@/object-record/record-show/types/RecordLayout';
|
import { RecordLayout } from '@/object-record/record-show/types/RecordLayout';
|
||||||
import { SingleTabProps } from '@/ui/layout/tab/components/TabList';
|
import { RecordLayoutTab } from '@/ui/layout/tab-list/types/RecordLayoutTab';
|
||||||
import { RecordLayoutTab } from '@/ui/layout/tab/types/RecordLayoutTab';
|
import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { RecordLayoutTab } from '@/ui/layout/tab/types/RecordLayoutTab';
|
import { RecordLayoutTab } from '@/ui/layout/tab-list/types/RecordLayoutTab';
|
||||||
|
|
||||||
export type RecordLayout = {
|
export type RecordLayout = {
|
||||||
hideSummaryAndFields?: boolean;
|
hideSummaryAndFields?: boolean;
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/comp
|
|||||||
import { SettingsAccountsCalendarChannelsGeneral } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral';
|
import { SettingsAccountsCalendarChannelsGeneral } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral';
|
||||||
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-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|||||||
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
|
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
|
||||||
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-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
|||||||
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
|
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
|
||||||
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 { TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui/display';
|
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui/display';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { userLookupResultState } from '@/settings/admin-panel/states/userLookupR
|
|||||||
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-list/components/TabList';
|
||||||
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';
|
||||||
@ -18,10 +18,9 @@ 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_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
|
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
|
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
|
||||||
import { Button } from 'twenty-ui/input';
|
|
||||||
import {
|
import {
|
||||||
H2Title,
|
H2Title,
|
||||||
IconId,
|
IconId,
|
||||||
@ -29,6 +28,7 @@ import {
|
|||||||
IconSearch,
|
IconSearch,
|
||||||
IconUser,
|
IconUser,
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
|
import { Button } from 'twenty-ui/input';
|
||||||
import { Section } from 'twenty-ui/layout';
|
import { Section } from 'twenty-ui/layout';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-vari
|
|||||||
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 { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
export const SettingsAdminTabContent = () => {
|
export const SettingsAdminTabContent = () => {
|
||||||
|
|||||||
@ -14,8 +14,8 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
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 { 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-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
|
import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
|
||||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
|
import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
|
||||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||||
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
|
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|||||||
@ -5,22 +5,22 @@ import {
|
|||||||
import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId';
|
import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId';
|
||||||
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-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/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 { 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 { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
|
||||||
import { Button, CoreEditorHeader } from 'twenty-ui/input';
|
|
||||||
import {
|
import {
|
||||||
H2Title,
|
H2Title,
|
||||||
IconGitCommit,
|
IconGitCommit,
|
||||||
IconPlayerPlay,
|
IconPlayerPlay,
|
||||||
IconRestore,
|
IconRestore,
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
|
import { Button, CoreEditorHeader } from 'twenty-ui/input';
|
||||||
import { Section } from 'twenty-ui/layout';
|
import { Section } from 'twenty-ui/layout';
|
||||||
|
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||||
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
|
|
||||||
const StyledTabList = styled(TabList)`
|
const StyledTabList = styled(TabList)`
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import { RecordLayout } from '@/object-record/record-show/types/RecordLayout';
|
|||||||
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 { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
|
||||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
|
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||||
|
import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||||
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 { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
@ -28,7 +30,13 @@ const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTabListContainer = styled.div<{ shouldDisplay: boolean }>`
|
const StyledTabListContainer = styled.div<{ shouldDisplay: boolean }>`
|
||||||
display: ${({ shouldDisplay }) => (shouldDisplay ? 'flex' : 'none')};
|
${({ shouldDisplay }) =>
|
||||||
|
!shouldDisplay &&
|
||||||
|
`
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTabList = styled(TabList)`
|
const StyledTabList = styled(TabList)`
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Avatar } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export const TabAvatar = ({ tab }: { tab: SingleTabProps }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (isDefined(tab.logo)) {
|
||||||
|
return <Avatar avatarUrl={tab.logo} size="md" placeholder={tab.title} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
tab.Icon && (
|
||||||
|
<tab.Icon
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
color={
|
||||||
|
tab.disabled ? theme.font.color.tertiary : theme.font.color.secondary
|
||||||
|
}
|
||||||
|
stroke={theme.icon.stroke.md}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap';
|
||||||
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
|
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||||
|
import { TabListProps } from '@/ui/layout/tab-list/types/TabListProps';
|
||||||
|
import { TabWidthsById } from '@/ui/layout/tab-list/types/TabWidthsById';
|
||||||
|
import { calculateVisibleTabCount } from '@/ui/layout/tab-list/utils/calculateVisibleTabCount';
|
||||||
|
import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
|
||||||
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { TabButton } from 'twenty-ui/input';
|
||||||
|
import { TabListDropdown } from './TabListDropdown';
|
||||||
|
import { TabListFromUrlOptionalEffect } from './TabListFromUrlOptionalEffect';
|
||||||
|
import { TabMoreButton } from './TabMoreButton';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
height: ${({ theme }) => theme.spacing(10)};
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: ${({ theme }) => theme.border.color.light};
|
||||||
|
bottom: 0;
|
||||||
|
content: '';
|
||||||
|
height: 1px;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTabContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${TAB_LIST_GAP}px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledHiddenMeasurement = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${TAB_LIST_GAP}px;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: -9999px;
|
||||||
|
visibility: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TabList = ({
|
||||||
|
tabs,
|
||||||
|
loading,
|
||||||
|
behaveAsLinks = true,
|
||||||
|
isInRightDrawer,
|
||||||
|
className,
|
||||||
|
componentInstanceId,
|
||||||
|
}: TabListProps) => {
|
||||||
|
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [activeTabId, setActiveTabId] = useRecoilComponentStateV2(
|
||||||
|
activeTabIdComponentState,
|
||||||
|
componentInstanceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tabWidthsById, setTabWidthsById] = useState<TabWidthsById>({});
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
const [moreButtonWidth, setMoreButtonWidth] = useState(0);
|
||||||
|
|
||||||
|
const initialActiveTabId = activeTabId || visibleTabs[0]?.id || '';
|
||||||
|
|
||||||
|
const visibleTabCount = useMemo(() => {
|
||||||
|
return calculateVisibleTabCount({
|
||||||
|
visibleTabs,
|
||||||
|
tabWidthsById,
|
||||||
|
containerWidth,
|
||||||
|
moreButtonWidth,
|
||||||
|
});
|
||||||
|
}, [tabWidthsById, containerWidth, moreButtonWidth, visibleTabs]);
|
||||||
|
|
||||||
|
const hiddenTabsCount = visibleTabs.length - visibleTabCount;
|
||||||
|
const hasHiddenTabs = hiddenTabsCount > 0;
|
||||||
|
|
||||||
|
const dropdownId = `tab-overflow-${componentInstanceId}`;
|
||||||
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
|
const isActiveTabHidden = useMemo(() => {
|
||||||
|
if (!hasHiddenTabs) return false;
|
||||||
|
const hiddenTabs = visibleTabs.slice(visibleTabCount);
|
||||||
|
return hiddenTabs.some((tab) => tab.id === activeTabId);
|
||||||
|
}, [hasHiddenTabs, visibleTabs, visibleTabCount, activeTabId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTabId(initialActiveTabId);
|
||||||
|
}, [initialActiveTabId, setActiveTabId]);
|
||||||
|
|
||||||
|
const handleTabSelect = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
setActiveTabId(tabId);
|
||||||
|
},
|
||||||
|
[setActiveTabId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTabSelectFromDropdown = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
if (behaveAsLinks) {
|
||||||
|
navigate(`#${tabId}`);
|
||||||
|
} else {
|
||||||
|
handleTabSelect(tabId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[behaveAsLinks, handleTabSelect, navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTabWidthChange = useCallback(
|
||||||
|
(tabId: string) => (dimensions: { width: number; height: number }) => {
|
||||||
|
setTabWidthsById((prev) => {
|
||||||
|
if (prev[tabId] !== dimensions.width) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[tabId]: dimensions.width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleContainerWidthChange = useCallback(
|
||||||
|
(dimensions: { width: number; height: number }) => {
|
||||||
|
setContainerWidth((prev) => {
|
||||||
|
return prev !== dimensions.width ? dimensions.width : prev;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMoreButtonWidthChange = useCallback(
|
||||||
|
(dimensions: { width: number; height: number }) => {
|
||||||
|
setMoreButtonWidth((prev) => {
|
||||||
|
return prev !== dimensions.width ? dimensions.width : prev;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (visibleTabs.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabListComponentInstanceContext.Provider
|
||||||
|
value={{ instanceId: componentInstanceId }}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<TabListFromUrlOptionalEffect
|
||||||
|
isInRightDrawer={!!isInRightDrawer}
|
||||||
|
tabListIds={tabs.map((tab) => tab.id)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{visibleTabs.length > 1 && (
|
||||||
|
<StyledHiddenMeasurement>
|
||||||
|
{visibleTabs.map((tab) => (
|
||||||
|
<NodeDimension
|
||||||
|
key={tab.id}
|
||||||
|
onDimensionChange={handleTabWidthChange(tab.id)}
|
||||||
|
>
|
||||||
|
<TabButton
|
||||||
|
id={tab.id}
|
||||||
|
title={tab.title}
|
||||||
|
LeftIcon={tab.Icon}
|
||||||
|
logo={tab.logo}
|
||||||
|
active={tab.id === activeTabId}
|
||||||
|
disabled={tab.disabled ?? loading}
|
||||||
|
pill={tab.pill}
|
||||||
|
disableTestId={true}
|
||||||
|
/>
|
||||||
|
</NodeDimension>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<NodeDimension onDimensionChange={handleMoreButtonWidthChange}>
|
||||||
|
<TabMoreButton hiddenTabsCount={1} active={false} />
|
||||||
|
</NodeDimension>
|
||||||
|
</StyledHiddenMeasurement>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NodeDimension onDimensionChange={handleContainerWidthChange}>
|
||||||
|
<StyledContainer className={className}>
|
||||||
|
<StyledTabContainer>
|
||||||
|
{visibleTabs.slice(0, visibleTabCount).map((tab) => (
|
||||||
|
<TabButton
|
||||||
|
key={tab.id}
|
||||||
|
id={tab.id}
|
||||||
|
title={tab.title}
|
||||||
|
LeftIcon={tab.Icon}
|
||||||
|
logo={tab.logo}
|
||||||
|
active={tab.id === activeTabId}
|
||||||
|
disabled={tab.disabled ?? loading}
|
||||||
|
pill={tab.pill}
|
||||||
|
to={behaveAsLinks ? `#${tab.id}` : undefined}
|
||||||
|
onClick={
|
||||||
|
behaveAsLinks ? undefined : () => handleTabSelect(tab.id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledTabContainer>
|
||||||
|
|
||||||
|
{hasHiddenTabs && (
|
||||||
|
<TabListDropdown
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
onClose={() => {
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
overflow={{
|
||||||
|
hiddenTabsCount,
|
||||||
|
isActiveTabHidden,
|
||||||
|
}}
|
||||||
|
hiddenTabs={visibleTabs.slice(visibleTabCount)}
|
||||||
|
activeTabId={activeTabId || ''}
|
||||||
|
onTabSelect={handleTabSelectFromDropdown}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledContainer>
|
||||||
|
</NodeDimension>
|
||||||
|
</>
|
||||||
|
</TabListComponentInstanceContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { TabAvatar } from '@/ui/layout/tab-list/components/TabAvatar';
|
||||||
|
import { TabMoreButton } from '@/ui/layout/tab-list/components/TabMoreButton';
|
||||||
|
import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||||
|
import { MenuItemSelectAvatar } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
|
type TabListDropdownProps = {
|
||||||
|
dropdownId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
overflow: {
|
||||||
|
hiddenTabsCount: number;
|
||||||
|
isActiveTabHidden: boolean;
|
||||||
|
};
|
||||||
|
hiddenTabs: SingleTabProps[];
|
||||||
|
activeTabId: string | null;
|
||||||
|
onTabSelect: (tabId: string) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabListDropdown = ({
|
||||||
|
dropdownId,
|
||||||
|
onClose,
|
||||||
|
overflow,
|
||||||
|
hiddenTabs,
|
||||||
|
activeTabId,
|
||||||
|
onTabSelect,
|
||||||
|
loading,
|
||||||
|
}: TabListDropdownProps) => {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
dropdownPlacement="bottom-end"
|
||||||
|
onClickOutside={onClose}
|
||||||
|
dropdownOffset={{ x: 0, y: 8 }}
|
||||||
|
clickableComponent={
|
||||||
|
<TabMoreButton
|
||||||
|
hiddenTabsCount={overflow.hiddenTabsCount}
|
||||||
|
active={overflow.isActiveTabHidden}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownContent>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
{hiddenTabs.map((tab) => {
|
||||||
|
const isDisabled = tab.disabled ?? loading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItemSelectAvatar
|
||||||
|
key={tab.id}
|
||||||
|
text={tab.title}
|
||||||
|
avatar={<TabAvatar tab={tab} />}
|
||||||
|
selected={tab.id === activeTabId}
|
||||||
|
onClick={
|
||||||
|
isDisabled
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
onTabSelect(tab.id);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={isDisabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownContent>
|
||||||
|
}
|
||||||
|
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { IconChevronDown } from 'twenty-ui/display';
|
||||||
|
import { TabButton } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
const StyledTabMoreButton = styled(TabButton)`
|
||||||
|
height: ${({ theme }) => theme.spacing(10)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TabMoreButton = ({
|
||||||
|
hiddenTabsCount,
|
||||||
|
active,
|
||||||
|
}: {
|
||||||
|
hiddenTabsCount: number;
|
||||||
|
active: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledTabMoreButton
|
||||||
|
id="tab-more-button"
|
||||||
|
active={active}
|
||||||
|
title={`+${hiddenTabsCount} ${t`More`}`}
|
||||||
|
RightIcon={IconChevronDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { TabMoreButton } from '../TabMoreButton';
|
||||||
|
|
||||||
|
const meta: Meta<typeof TabMoreButton> = {
|
||||||
|
title: 'UI/Layout/TabList/TabMoreButton',
|
||||||
|
component: TabMoreButton,
|
||||||
|
args: {
|
||||||
|
hiddenTabsCount: 3,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof TabMoreButton>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
hiddenTabsCount: 3,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Active: Story = {
|
||||||
|
args: {
|
||||||
|
hiddenTabsCount: 5,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
IconCalendar,
|
||||||
|
IconCheckbox,
|
||||||
|
IconHeart,
|
||||||
|
IconHome,
|
||||||
|
IconMail,
|
||||||
|
IconPhone,
|
||||||
|
IconUser,
|
||||||
|
} from 'twenty-ui/display';
|
||||||
|
import { ComponentWithRouterDecorator } from 'twenty-ui/testing';
|
||||||
|
import { TabList } from '../TabList';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'general', title: 'General', logo: 'https://picsum.photos/200' },
|
||||||
|
{ id: 'contacts', title: 'Contacts', Icon: IconUser },
|
||||||
|
{ id: 'messages', title: 'Messages', Icon: IconMail },
|
||||||
|
{ id: 'calls', title: 'Calls', Icon: IconPhone },
|
||||||
|
{ id: 'calendar', title: 'Calendar', Icon: IconCalendar },
|
||||||
|
{ id: 'sales', title: 'Sales', Icon: IconHome, disabled: true },
|
||||||
|
{ id: 'hidden', title: 'Hidden Tab', Icon: IconCheckbox, hide: true },
|
||||||
|
{
|
||||||
|
id: 'time',
|
||||||
|
title: 'Time Tracking',
|
||||||
|
logo: 'https://picsum.photos/192/192',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'activity',
|
||||||
|
title: 'Activity',
|
||||||
|
logo: 'https://twenty-front-screenshots.s3.eu-west-3.amazonaws.com/server-icon.png',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{ id: 'favorites', title: 'Favorites', Icon: IconHeart },
|
||||||
|
{ id: 'reports', title: 'Reports', Icon: IconCheckbox },
|
||||||
|
];
|
||||||
|
|
||||||
|
const StyledInteractiveContainer = styled.div`
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.strong};
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: ${({ theme }) => theme.spacing(5)};
|
||||||
|
resize: horizontal;
|
||||||
|
width: 600px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const meta: Meta<typeof TabList> = {
|
||||||
|
title: 'UI/Layout/TabList/TabList',
|
||||||
|
component: TabList,
|
||||||
|
args: {
|
||||||
|
tabs: tabs,
|
||||||
|
componentInstanceId: 'tab-list',
|
||||||
|
},
|
||||||
|
decorators: [ComponentWithRouterDecorator],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof TabList>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
tabs: tabs,
|
||||||
|
componentInstanceId: 'resizable-tabs',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledInteractiveContainer>
|
||||||
|
<p>
|
||||||
|
<strong>↔ Drag the bottom-right corner to resize!</strong>
|
||||||
|
</p>
|
||||||
|
<TabList
|
||||||
|
tabs={args.tabs}
|
||||||
|
componentInstanceId={args.componentInstanceId}
|
||||||
|
loading={args.loading}
|
||||||
|
behaveAsLinks={args.behaveAsLinks}
|
||||||
|
isInRightDrawer={args.isInRightDrawer}
|
||||||
|
className={args.className}
|
||||||
|
/>
|
||||||
|
</StyledInteractiveContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const TAB_LIST_GAP = 4;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const TAB_LIST_LEFT_PADDING = 8;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
|
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||||
|
|
||||||
export const activeTabIdComponentState = createComponentStateV2<string | null>({
|
export const activeTabIdComponentState = createComponentStateV2<string | null>({
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
|
import { LayoutCard } from '@/ui/layout/tab-list/types/LayoutCard';
|
||||||
import { TabVisibilityConfig } from '@/ui/layout/tab/types/TabVisibilityConfig';
|
import { TabVisibilityConfig } from '@/ui/layout/tab-list/types/TabVisibilityConfig';
|
||||||
import { IconComponent } from 'twenty-ui/display';
|
import { IconComponent } from 'twenty-ui/display';
|
||||||
|
|
||||||
export type RecordLayoutTab = {
|
export type RecordLayoutTab = {
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { LayoutCard } from '@/ui/layout/tab-list/types/LayoutCard';
|
||||||
|
import { IconComponent } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export type SingleTabProps<T extends string = string> = {
|
||||||
|
title: string;
|
||||||
|
Icon?: IconComponent;
|
||||||
|
id: T;
|
||||||
|
hide?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
pill?: string | React.ReactElement;
|
||||||
|
cards?: LayoutCard[];
|
||||||
|
logo?: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||||
|
|
||||||
|
export type TabListProps = {
|
||||||
|
tabs: SingleTabProps[];
|
||||||
|
loading?: boolean;
|
||||||
|
behaveAsLinks?: boolean;
|
||||||
|
className?: string;
|
||||||
|
isInRightDrawer?: boolean;
|
||||||
|
componentInstanceId: string;
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type TabWidthsById = Record<string, number>;
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap';
|
||||||
|
import { TAB_LIST_LEFT_PADDING } from '@/ui/layout/tab-list/constants/TabListPadding';
|
||||||
|
import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||||
|
import { TabWidthsById } from '@/ui/layout/tab-list/types/TabWidthsById';
|
||||||
|
|
||||||
|
type CalculateVisibleTabCountParams = {
|
||||||
|
visibleTabs: SingleTabProps[];
|
||||||
|
tabWidthsById: TabWidthsById;
|
||||||
|
containerWidth: number;
|
||||||
|
moreButtonWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateVisibleTabCount = ({
|
||||||
|
visibleTabs,
|
||||||
|
tabWidthsById,
|
||||||
|
containerWidth,
|
||||||
|
moreButtonWidth,
|
||||||
|
}: CalculateVisibleTabCountParams): number => {
|
||||||
|
if (Object.keys(tabWidthsById).length === 0 || containerWidth === 0) {
|
||||||
|
return visibleTabs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableWidth = containerWidth - TAB_LIST_LEFT_PADDING;
|
||||||
|
|
||||||
|
let totalWidth = 0;
|
||||||
|
for (let i = 0; i < visibleTabs.length; i++) {
|
||||||
|
const tab = visibleTabs[i];
|
||||||
|
const tabWidth = tabWidthsById[tab.id];
|
||||||
|
|
||||||
|
// Skip if width not measured yet
|
||||||
|
if (tabWidth === undefined) {
|
||||||
|
return visibleTabs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gapsWidth = i > 0 ? TAB_LIST_GAP : 0;
|
||||||
|
const potentialMoreButtonWidth =
|
||||||
|
i < visibleTabs.length - 1 ? moreButtonWidth + TAB_LIST_GAP : 0;
|
||||||
|
|
||||||
|
totalWidth += tabWidth + gapsWidth;
|
||||||
|
|
||||||
|
if (totalWidth + potentialMoreButtonWidth > availableWidth) {
|
||||||
|
return Math.max(1, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visibleTabs.length;
|
||||||
|
};
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
|
||||||
import isPropValid from '@emotion/is-prop-valid';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { ReactElement } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Pill } from 'twenty-ui/components';
|
|
||||||
import { Avatar, IconComponent } from 'twenty-ui/display';
|
|
||||||
import { useMouseDownNavigation } from 'twenty-ui/utilities';
|
|
||||||
|
|
||||||
type TabProps = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
Icon?: IconComponent;
|
|
||||||
active?: boolean;
|
|
||||||
className?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
pill?: string | ReactElement;
|
|
||||||
to?: string;
|
|
||||||
logo?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledTab = styled('button', {
|
|
||||||
shouldForwardProp: (prop) => isPropValid(prop) && prop !== 'active',
|
|
||||||
})<{ active?: boolean; disabled?: boolean; to?: string }>`
|
|
||||||
all: unset;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
border-color: ${({ theme, active }) =>
|
|
||||||
active ? theme.border.color.inverted : 'transparent'};
|
|
||||||
color: ${({ theme, active, disabled }) =>
|
|
||||||
active
|
|
||||||
? theme.font.color.primary
|
|
||||||
: disabled
|
|
||||||
? theme.font.color.light
|
|
||||||
: theme.font.color.secondary};
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: transparent;
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
border-top: none;
|
|
||||||
font-family: inherit;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
padding: ${({ theme }) => theme.spacing(2) + ' 0'};
|
|
||||||
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
|
|
||||||
text-decoration: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledHover = styled.span`
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
|
||||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
width: 100%;
|
|
||||||
&:hover {
|
|
||||||
background: ${({ theme }) => theme.background.tertiary};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background: ${({ theme }) => theme.background.quaternary};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledIconContainer = styled.div`
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Tab = ({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
Icon,
|
|
||||||
active = false,
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
disabled,
|
|
||||||
pill,
|
|
||||||
to,
|
|
||||||
logo,
|
|
||||||
}: TabProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { onClick: handleClick, onMouseDown: handleMouseDown } =
|
|
||||||
useMouseDownNavigation({
|
|
||||||
to,
|
|
||||||
onClick,
|
|
||||||
disabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconColor = active
|
|
||||||
? theme.font.color.primary
|
|
||||||
: disabled
|
|
||||||
? theme.font.color.light
|
|
||||||
: theme.font.color.secondary;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledTab
|
|
||||||
onClick={handleClick}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
active={active}
|
|
||||||
className={className}
|
|
||||||
disabled={disabled}
|
|
||||||
data-testid={'tab-' + id}
|
|
||||||
as={to ? Link : 'button'}
|
|
||||||
to={to}
|
|
||||||
>
|
|
||||||
<StyledHover>
|
|
||||||
<StyledIconContainer>
|
|
||||||
{logo && (
|
|
||||||
<Avatar
|
|
||||||
avatarUrl={logo}
|
|
||||||
size="md"
|
|
||||||
placeholder={title}
|
|
||||||
iconColor={iconColor}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{Icon && (
|
|
||||||
<Avatar
|
|
||||||
Icon={Icon}
|
|
||||||
size="md"
|
|
||||||
placeholder={title}
|
|
||||||
iconColor={iconColor}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledIconContainer>
|
|
||||||
<EllipsisDisplay>{title}</EllipsisDisplay>
|
|
||||||
{pill && typeof pill === 'string' ? <Pill label={pill} /> : pill}
|
|
||||||
</StyledHover>
|
|
||||||
</StyledTab>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
|
|
||||||
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 React, { useEffect } from 'react';
|
|
||||||
import { IconComponent } from 'twenty-ui/display';
|
|
||||||
import { Tab } from './Tab';
|
|
||||||
|
|
||||||
export type SingleTabProps<T extends string = string> = {
|
|
||||||
title: string;
|
|
||||||
Icon?: IconComponent;
|
|
||||||
id: T;
|
|
||||||
hide?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
pill?: string | React.ReactElement;
|
|
||||||
cards?: LayoutCard[];
|
|
||||||
logo?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TabListProps = {
|
|
||||||
tabs: SingleTabProps[];
|
|
||||||
loading?: boolean;
|
|
||||||
behaveAsLinks?: boolean;
|
|
||||||
className?: string;
|
|
||||||
isInRightDrawer?: boolean;
|
|
||||||
componentInstanceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
height: 40px;
|
|
||||||
user-select: none;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledOuterContainer = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TabList = ({
|
|
||||||
tabs,
|
|
||||||
loading,
|
|
||||||
behaveAsLinks = true,
|
|
||||||
isInRightDrawer,
|
|
||||||
className,
|
|
||||||
componentInstanceId,
|
|
||||||
}: TabListProps) => {
|
|
||||||
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
|
||||||
|
|
||||||
const [activeTabId, setActiveTabId] = useRecoilComponentStateV2(
|
|
||||||
activeTabIdComponentState,
|
|
||||||
componentInstanceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialActiveTabId = activeTabId || visibleTabs[0]?.id || '';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setActiveTabId(initialActiveTabId);
|
|
||||||
}, [initialActiveTabId, setActiveTabId]);
|
|
||||||
|
|
||||||
if (visibleTabs.length <= 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabListComponentInstanceContext.Provider
|
|
||||||
value={{ instanceId: componentInstanceId }}
|
|
||||||
>
|
|
||||||
<StyledOuterContainer>
|
|
||||||
<TabListFromUrlOptionalEffect
|
|
||||||
isInRightDrawer={!!isInRightDrawer}
|
|
||||||
tabListIds={tabs.map((tab) => tab.id)}
|
|
||||||
/>
|
|
||||||
<ScrollWrapper
|
|
||||||
defaultEnableYScroll={false}
|
|
||||||
componentInstanceId={`scroll-wrapper-tab-list-${componentInstanceId}`}
|
|
||||||
>
|
|
||||||
<StyledContainer className={className}>
|
|
||||||
{visibleTabs.map((tab) => (
|
|
||||||
<Tab
|
|
||||||
id={tab.id}
|
|
||||||
key={tab.id}
|
|
||||||
title={tab.title}
|
|
||||||
Icon={tab.Icon}
|
|
||||||
logo={tab.logo}
|
|
||||||
active={tab.id === activeTabId}
|
|
||||||
disabled={tab.disabled ?? loading}
|
|
||||||
pill={tab.pill}
|
|
||||||
to={behaveAsLinks ? `#${tab.id}` : undefined}
|
|
||||||
onClick={
|
|
||||||
behaveAsLinks
|
|
||||||
? undefined
|
|
||||||
: () => {
|
|
||||||
setActiveTabId(tab.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StyledContainer>
|
|
||||||
</ScrollWrapper>
|
|
||||||
</StyledOuterContainer>
|
|
||||||
</TabListComponentInstanceContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
|
|
||||||
import { IconCheckbox } from 'twenty-ui/display';
|
|
||||||
import {
|
|
||||||
CatalogDecorator,
|
|
||||||
CatalogStory,
|
|
||||||
ComponentWithRouterDecorator,
|
|
||||||
} from 'twenty-ui/testing';
|
|
||||||
import { Tab } from '../Tab';
|
|
||||||
|
|
||||||
const meta: Meta<typeof Tab> = {
|
|
||||||
title: 'UI/Layout/Tab/Tab',
|
|
||||||
component: Tab,
|
|
||||||
decorators: [ComponentWithRouterDecorator],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof Tab>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
title: 'Tab title',
|
|
||||||
active: false,
|
|
||||||
Icon: IconCheckbox,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
decorators: [ComponentWithRouterDecorator],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Catalog: CatalogStory<Story, typeof Tab> = {
|
|
||||||
args: { title: 'Tab title', Icon: IconCheckbox },
|
|
||||||
argTypes: {
|
|
||||||
active: { control: false },
|
|
||||||
disabled: { control: false },
|
|
||||||
onClick: { control: false },
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
pseudo: { hover: ['.hover'], active: ['.active'] },
|
|
||||||
catalog: {
|
|
||||||
dimensions: [
|
|
||||||
{
|
|
||||||
name: 'states',
|
|
||||||
values: ['default', 'hover', 'active'],
|
|
||||||
props: (state: string) =>
|
|
||||||
state === 'default' ? {} : { className: state },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Active',
|
|
||||||
values: ['true', 'false'],
|
|
||||||
labels: (active: string) =>
|
|
||||||
active === 'true' ? 'active' : 'inactive',
|
|
||||||
props: (active: string) => ({ active: active === 'true' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Disabled',
|
|
||||||
values: ['true', 'false'],
|
|
||||||
labels: (disabled: string) =>
|
|
||||||
disabled === 'true' ? 'disabled' : 'enabled',
|
|
||||||
props: (disabled: string) => ({ disabled: disabled === 'true' }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
decorators: [CatalogDecorator],
|
|
||||||
};
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { expect, within } from '@storybook/test';
|
|
||||||
|
|
||||||
import { TabList } from '../TabList';
|
|
||||||
import { ComponentWithRouterDecorator } from 'twenty-ui/testing';
|
|
||||||
import { IconCheckbox } from 'twenty-ui/display';
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Tab1',
|
|
||||||
Icon: IconCheckbox,
|
|
||||||
hide: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Tab2',
|
|
||||||
Icon: IconCheckbox,
|
|
||||||
hide: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'Tab3',
|
|
||||||
Icon: IconCheckbox,
|
|
||||||
hide: false,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
title: 'Tab4',
|
|
||||||
Icon: IconCheckbox,
|
|
||||||
hide: false,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const meta: Meta<typeof TabList> = {
|
|
||||||
title: 'UI/Layout/Tab/TabList',
|
|
||||||
component: TabList,
|
|
||||||
args: {
|
|
||||||
tabs: tabs,
|
|
||||||
componentInstanceId: 'tab-list',
|
|
||||||
},
|
|
||||||
decorators: [ComponentWithRouterDecorator],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof TabList>;
|
|
||||||
|
|
||||||
export const TabListDisplay: Story = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const submitButton = canvas.queryByText('Tab1');
|
|
||||||
expect(submitButton).toBeNull();
|
|
||||||
expect(await canvas.findByText('Tab2')).toBeInTheDocument();
|
|
||||||
expect(await canvas.findByText('Tab3')).toBeInTheDocument();
|
|
||||||
expect(await canvas.findByText('Tab4')).toBeInTheDocument();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { ReactNode, useEffect, useRef } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
type NodeDimensionProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
onDimensionChange: (dimensions: { width: number; height: number }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeDimension = ({
|
||||||
|
children,
|
||||||
|
onDimensionChange,
|
||||||
|
}: NodeDimensionProps) => {
|
||||||
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!elementRef.current) return;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
const entry = entries[0];
|
||||||
|
if (isDefined(entry)) {
|
||||||
|
onDimensionChange({
|
||||||
|
width: entry.contentRect.width,
|
||||||
|
height: entry.contentRect.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(elementRef.current);
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect();
|
||||||
|
}, [onDimensionChange]);
|
||||||
|
|
||||||
|
return <div ref={elementRef}>{children}</div>;
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
||||||
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
||||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||||
|
|||||||
@ -18,8 +18,8 @@ import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-function
|
|||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
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-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
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';
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isReco
|
|||||||
|
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState';
|
import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState';
|
||||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
|
|||||||
@ -11,13 +11,13 @@ import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLab
|
|||||||
import { AppPath } from '@/types/AppPath';
|
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-list/components/TabList';
|
||||||
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 { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
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/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|||||||
@ -11,16 +11,16 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
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 { 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-list/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui/display';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui/display';
|
|
||||||
|
|
||||||
const SERVERLESS_FUNCTION_DETAIL_ID = 'serverless-function-detail';
|
const SERVERLESS_FUNCTION_DETAIL_ID = 'serverless-function-detail';
|
||||||
|
|
||||||
|
|||||||
122
packages/twenty-ui/src/input/button/components/TabButton.tsx
Normal file
122
packages/twenty-ui/src/input/button/components/TabButton.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Pill } from '@ui/components/Pill/Pill';
|
||||||
|
import { Avatar, IconComponent } from '@ui/display';
|
||||||
|
import { ThemeContext } from '@ui/theme';
|
||||||
|
import { ReactElement, useContext } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const StyledTabButton = styled.button<{
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
to?: string;
|
||||||
|
}>`
|
||||||
|
all: unset;
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme, active, disabled }) =>
|
||||||
|
active
|
||||||
|
? theme.font.color.primary
|
||||||
|
: disabled
|
||||||
|
? theme.font.color.light
|
||||||
|
: theme.font.color.secondary};
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-family: inherit;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: ${({ theme, active }) =>
|
||||||
|
active ? theme.border.color.inverted : 'transparent'};
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTabHover = styled.span<{
|
||||||
|
contentSize?: 'sm' | 'md';
|
||||||
|
}>`
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: ${({ theme, contentSize }) =>
|
||||||
|
contentSize === 'sm'
|
||||||
|
? `${theme.spacing(1)} ${theme.spacing(2)}`
|
||||||
|
: `${theme.spacing(2)} ${theme.spacing(2)}`};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme }) => theme.background.tertiary};
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background: ${({ theme }) => theme.background.quaternary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type TabButtonProps = {
|
||||||
|
id: string;
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
to?: string;
|
||||||
|
LeftIcon?: IconComponent;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
logo?: string;
|
||||||
|
RightIcon?: IconComponent;
|
||||||
|
pill?: string | ReactElement;
|
||||||
|
contentSize?: 'sm' | 'md';
|
||||||
|
disableTestId?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabButton = ({
|
||||||
|
id,
|
||||||
|
active,
|
||||||
|
disabled,
|
||||||
|
to,
|
||||||
|
LeftIcon,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
logo,
|
||||||
|
RightIcon,
|
||||||
|
pill,
|
||||||
|
contentSize = 'sm',
|
||||||
|
disableTestId = false,
|
||||||
|
}: TabButtonProps) => {
|
||||||
|
const { theme } = useContext(ThemeContext);
|
||||||
|
const iconColor = active
|
||||||
|
? theme.font.color.primary
|
||||||
|
: disabled
|
||||||
|
? theme.font.color.extraLight
|
||||||
|
: theme.font.color.secondary;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledTabButton
|
||||||
|
data-testid={disableTestId ? undefined : `tab-${id}`}
|
||||||
|
active={active}
|
||||||
|
disabled={disabled}
|
||||||
|
as={to ? Link : 'button'}
|
||||||
|
to={to}
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<StyledTabHover contentSize={contentSize}>
|
||||||
|
{LeftIcon && <LeftIcon color={iconColor} size={theme.icon.size.md} />}
|
||||||
|
{logo && <Avatar avatarUrl={logo} size="md" placeholder={title} />}
|
||||||
|
{title}
|
||||||
|
{RightIcon && <RightIcon color={iconColor} size={theme.icon.size.md} />}
|
||||||
|
{pill && (typeof pill === 'string' ? <Pill label={pill} /> : pill)}
|
||||||
|
</StyledTabHover>
|
||||||
|
</StyledTabButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,377 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
IconCheckbox,
|
||||||
|
IconChevronDown,
|
||||||
|
IconMail,
|
||||||
|
IconSearch,
|
||||||
|
IconSettings,
|
||||||
|
IconUser,
|
||||||
|
} from '@ui/display';
|
||||||
|
import {
|
||||||
|
CatalogDecorator,
|
||||||
|
CatalogStory,
|
||||||
|
ComponentWithRouterDecorator,
|
||||||
|
RecoilRootDecorator,
|
||||||
|
} from '@ui/testing';
|
||||||
|
import { TabButton } from '../TabButton';
|
||||||
|
|
||||||
|
// Mimic the TabList container styling for proper positioning
|
||||||
|
const StyledTabContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
height: 40px;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: ${({ theme }) => theme.border.color.light};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const meta: Meta<typeof TabButton> = {
|
||||||
|
title: 'UI/Input/Button/TabButton',
|
||||||
|
component: TabButton,
|
||||||
|
decorators: [ComponentWithRouterDecorator, RecoilRootDecorator],
|
||||||
|
args: {
|
||||||
|
id: 'tab-button',
|
||||||
|
title: 'Tab Title',
|
||||||
|
active: false,
|
||||||
|
disabled: false,
|
||||||
|
contentSize: 'sm',
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
LeftIcon: { control: false },
|
||||||
|
RightIcon: { control: false },
|
||||||
|
pill: { control: 'text' },
|
||||||
|
contentSize: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['sm', 'md'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof TabButton>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'General',
|
||||||
|
LeftIcon: IconSettings,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Active: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Active Tab',
|
||||||
|
LeftIcon: IconUser,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Disabled Tab',
|
||||||
|
LeftIcon: IconCheckbox,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithLogo: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Company',
|
||||||
|
logo: 'https://picsum.photos/192/192',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithStringPill: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Messages',
|
||||||
|
LeftIcon: IconMail,
|
||||||
|
pill: '12',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithBothIcons: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Search',
|
||||||
|
LeftIcon: IconSearch,
|
||||||
|
RightIcon: IconChevronDown,
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AsLink: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Link Tab',
|
||||||
|
LeftIcon: IconUser,
|
||||||
|
to: '/profile',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SmallContent: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Small',
|
||||||
|
LeftIcon: IconSettings,
|
||||||
|
contentSize: 'sm',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MediumContent: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Medium',
|
||||||
|
LeftIcon: IconSettings,
|
||||||
|
contentSize: 'md',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Catalog: CatalogStory<Story, typeof TabButton> = {
|
||||||
|
args: {
|
||||||
|
title: 'Tab title',
|
||||||
|
LeftIcon: IconCheckbox,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
active: { control: false },
|
||||||
|
disabled: { control: false },
|
||||||
|
onClick: { control: false },
|
||||||
|
to: { control: false },
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<StyledTabContainer>
|
||||||
|
<TabButton
|
||||||
|
id={args.id}
|
||||||
|
title={args.title}
|
||||||
|
LeftIcon={args.LeftIcon}
|
||||||
|
RightIcon={args.RightIcon}
|
||||||
|
active={args.active}
|
||||||
|
disabled={args.disabled}
|
||||||
|
pill={args.pill}
|
||||||
|
to={args.to}
|
||||||
|
logo={args.logo}
|
||||||
|
onClick={args.onClick}
|
||||||
|
className={args.className}
|
||||||
|
contentSize={args.contentSize}
|
||||||
|
/>
|
||||||
|
</StyledTabContainer>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
pseudo: { hover: ['.hover'], active: ['.active'] },
|
||||||
|
catalog: {
|
||||||
|
dimensions: [
|
||||||
|
{
|
||||||
|
name: 'states',
|
||||||
|
values: ['default', 'hover', 'active'],
|
||||||
|
props: (state: string) =>
|
||||||
|
state === 'default' ? {} : { className: state },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'State',
|
||||||
|
values: ['active', 'inactive', 'disabled'],
|
||||||
|
labels: (state: string) => state,
|
||||||
|
props: (state: string) => ({
|
||||||
|
active: state === 'active',
|
||||||
|
disabled: state === 'disabled',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Content Size',
|
||||||
|
values: ['sm', 'md'],
|
||||||
|
labels: (size: string) => size,
|
||||||
|
props: (size: string) => ({ contentSize: size as 'sm' | 'md' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Content',
|
||||||
|
values: ['icon', 'logo', 'pill'],
|
||||||
|
props: (content: string) => {
|
||||||
|
switch (content) {
|
||||||
|
case 'icon':
|
||||||
|
return { LeftIcon: IconSettings };
|
||||||
|
case 'logo':
|
||||||
|
return {
|
||||||
|
logo: 'https://picsum.photos/192/192',
|
||||||
|
LeftIcon: undefined,
|
||||||
|
};
|
||||||
|
case 'pill':
|
||||||
|
return { LeftIcon: IconMail, pill: '5' };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
layout: 'centered',
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: 'responsive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [CatalogDecorator],
|
||||||
|
};
|
||||||
@ -65,6 +65,7 @@ export { LightIconButtonGroup } from './button/components/LightIconButtonGroup';
|
|||||||
export type { MainButtonVariant } from './button/components/MainButton';
|
export type { MainButtonVariant } from './button/components/MainButton';
|
||||||
export { MainButton } from './button/components/MainButton';
|
export { MainButton } from './button/components/MainButton';
|
||||||
export { RoundedIconButton } from './button/components/RoundedIconButton';
|
export { RoundedIconButton } from './button/components/RoundedIconButton';
|
||||||
|
export { TabButton } from './button/components/TabButton';
|
||||||
export { CodeEditor } from './code-editor/components/CodeEditor';
|
export { CodeEditor } from './code-editor/components/CodeEditor';
|
||||||
export type { CoreEditorHeaderProps } from './code-editor/components/CodeEditorHeader';
|
export type { CoreEditorHeaderProps } from './code-editor/components/CodeEditorHeader';
|
||||||
export { CoreEditorHeader } from './code-editor/components/CodeEditorHeader';
|
export { CoreEditorHeader } from './code-editor/components/CodeEditorHeader';
|
||||||
|
|||||||
Reference in New Issue
Block a user