diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 5e250fbd180eb..3bde2e353a2e3 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -24,20 +24,6 @@ const SettingsRestPlayground = lazy(() => ), ); -const SettingsAccountsCalendars = lazy(() => - import('~/pages/settings/accounts/SettingsAccountsCalendars').then( - (module) => ({ - default: module.SettingsAccountsCalendars, - }), - ), -); - -const SettingsAccountsEmails = lazy(() => - import('~/pages/settings/accounts/SettingsAccountsEmails').then((module) => ({ - default: module.SettingsAccountsEmails, - })), -); - const SettingsAccountsConfiguration = lazy(() => import('~/pages/settings/accounts/SettingsAccountsConfiguration').then( (module) => ({ @@ -122,20 +108,20 @@ const SettingsLogicFunctionDetail = lazy(() => ), ); -const SettingsWorkspace = lazy(() => - import('~/pages/settings/SettingsWorkspace').then((module) => ({ - default: module.SettingsWorkspace, +const SettingsGeneral = lazy(() => + import('~/pages/settings/general/SettingsGeneral').then((module) => ({ + default: module.SettingsGeneral, })), ); const SettingsWorkspaceEmail = lazy(() => - import('~/pages/settings/SettingsWorkspaceEmail').then((module) => ({ + import('~/pages/settings/email/SettingsWorkspaceEmail').then((module) => ({ default: module.SettingsWorkspaceEmail, })), ); const SettingsWorkspaceEmailGroupChannelDetail = lazy(() => - import('~/pages/settings/workspace/SettingsWorkspaceEmailGroupChannelDetail').then( + import('~/pages/settings/email/SettingsWorkspaceEmailGroupChannelDetail').then( (module) => ({ default: module.SettingsWorkspaceEmailGroupChannelDetail, }), @@ -157,9 +143,11 @@ const SettingsCustomDomainPage = lazy(() => ); const SettingsApiWebhooks = lazy(() => - import('~/pages/settings/workspace/SettingsApiWebhooks').then((module) => ({ - default: module.SettingsApiWebhooks, - })), + import('~/pages/settings/api-webhooks/SettingsApiWebhooks').then( + (module) => ({ + default: module.SettingsApiWebhooks, + }), + ), ); const SettingsAI = lazy(() => @@ -319,13 +307,13 @@ const SettingsWorkspaceMember = lazy(() => ); const SettingsProfile = lazy(() => - import('~/pages/settings/SettingsProfile').then((module) => ({ + import('~/pages/settings/profile/SettingsProfile').then((module) => ({ default: module.SettingsProfile, })), ); const SettingsTwoFactorAuthenticationMethod = lazy(() => - import('~/pages/settings/SettingsTwoFactorAuthenticationMethod').then( + import('~/pages/settings/profile/SettingsTwoFactorAuthenticationMethod').then( (module) => ({ default: module.SettingsTwoFactorAuthenticationMethod, }), @@ -346,20 +334,34 @@ const SettingsAccounts = lazy(() => })), ); +const SettingsAccountsEmails = lazy(() => + import('~/pages/settings/accounts/SettingsAccountsEmails').then((module) => ({ + default: module.SettingsAccountsEmails, + })), +); + +const SettingsAccountsCalendars = lazy(() => + import('~/pages/settings/accounts/SettingsAccountsCalendars').then( + (module) => ({ + default: module.SettingsAccountsCalendars, + }), + ), +); + const SettingsBilling = lazy(() => - import('~/pages/settings/SettingsBilling').then((module) => ({ + import('~/pages/settings/billing/SettingsBilling').then((module) => ({ default: module.SettingsBilling, })), ); const SettingsUsage = lazy(() => - import('~/pages/settings/SettingsUsage').then((module) => ({ + import('~/pages/settings/billing/SettingsUsage').then((module) => ({ default: module.SettingsUsage, })), ); const SettingsUsageUserDetail = lazy(() => - import('~/pages/settings/SettingsUsageUserDetail').then((module) => ({ + import('~/pages/settings/billing/SettingsUsageUserDetail').then((module) => ({ default: module.SettingsUsageUserDetail, })), ); @@ -417,12 +419,6 @@ const SettingsObjectFieldEdit = lazy(() => ), ); -const SettingsSecurity = lazy(() => - import('~/pages/settings/security/SettingsSecurity').then((module) => ({ - default: module.SettingsSecurity, - })), -); - const SettingsSecuritySSOIdentifyProvider = lazy(() => import('~/pages/settings/security/SettingsSecuritySSOIdentifyProvider').then( (module) => ({ @@ -565,9 +561,9 @@ const SettingsAdminWorkspaceChatThread = lazy(() => ), ); -const SettingsUpdates = lazy(() => - import('~/pages/settings/updates/SettingsUpdates').then((module) => ({ - default: module.SettingsUpdates, +const SettingsCommunity = lazy(() => + import('~/pages/settings/community/SettingsCommunity').then((module) => ({ + default: module.SettingsCommunity, })), ); @@ -624,20 +620,20 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( > } /> } - /> - } + path={SettingsPath.AccountsEmails} + element={} /> } /> } + path={SettingsPath.NewAccount} + element={} + /> + } /> ( /> } > - } /> + } /> } @@ -916,6 +912,13 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( /> + + } + /> + ( /> } > - } /> } @@ -1018,7 +1020,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( /> } > - } /> + } /> diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx index 9b0f1ebaa4897..c15e4720d2621 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx @@ -1,70 +1,32 @@ -import { styled } from '@linaria/react'; - +import { type CalendarChannel } from '@/accounts/types/CalendarChannel'; import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails'; -import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId'; -import { useMyCalendarChannels } from '@/settings/accounts/hooks/useMyCalendarChannels'; -import { TabList } from '@/ui/layout/tab-list/components/TabList'; -import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; -import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId'; import React from 'react'; -import { CalendarChannelSyncStage } from 'twenty-shared/types'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; -const StyledCalenderContainer = styled.div` - padding-bottom: ${themeCssVariables.spacing[6]}; -`; +type SettingsAccountsCalendarChannelsContainerProps = { + calendarChannels: CalendarChannel[]; +}; -export const SettingsAccountsCalendarChannelsContainer = () => { - const activeTabId = useAtomComponentStateValue( - activeTabIdComponentState, +export const SettingsAccountsCalendarChannelsContainer = ({ + calendarChannels, +}: SettingsAccountsCalendarChannelsContainerProps) => { + const activeTabId = useSettingsActiveTabId( SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID, + calendarChannels.map((channel) => channel.id), ); - const { channels: allCalendarChannels } = useMyCalendarChannels(); - - const calendarChannels = allCalendarChannels.filter( - (channel) => - channel.syncStage !== CalendarChannelSyncStage.PENDING_CONFIGURATION, - ); - - const tabs = [ - ...calendarChannels.map((calendarChannel) => ({ - id: calendarChannel.id, - title: calendarChannel.handle, - })), - ]; - - if (!calendarChannels.length) { - return ; - } - return ( <> - {tabs.length > 1 && ( - - - - )} {calendarChannels.map((calendarChannel) => ( - {(calendarChannels.length === 1 || - calendarChannel.id === activeTabId) && ( + {calendarChannel.id === activeTabId && ( )} ))} - {/* TODO: remove or keep? */} - {/* {activeTabId === 'general' && ( - - )} */} ); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEditImapSmtpCaldavConnection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEditImapSmtpCaldavConnection.tsx index 08072ca8dbcb4..2915570c6d628 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEditImapSmtpCaldavConnection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEditImapSmtpCaldavConnection.tsx @@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; import { SettingsPath } from 'twenty-shared/types'; import { Loader } from 'twenty-ui/feedback'; @@ -64,7 +64,7 @@ export const SettingsAccountsEditImapSmtpCaldavConnection = () => { const renderForm = () => ( // oxlint-disable-next-line react/jsx-props-no-spreading - { existingProtocols={existingProtocols} /> - + ); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx index 0c695d6b9cd30..328ec49708d11 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx @@ -1,86 +1,30 @@ -import { styled } from '@linaria/react'; - +import { type MessageChannel } from '@/accounts/types/MessageChannel'; import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails'; import { SettingsAccountsSelectedMessageChannelEffect } from '@/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect'; -import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; -import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels'; -import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState'; -import { TabList } from '@/ui/layout/tab-list/components/TabList'; -import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; -import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import React, { useCallback } from 'react'; -import { - MessageChannelSyncStage, - MessageChannelType, -} from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId'; +import React from 'react'; -const StyledMessageContainer = styled.div` - padding-bottom: ${themeCssVariables.spacing[6]}; -`; +type SettingsAccountsMessageChannelsContainerProps = { + messageChannels: MessageChannel[]; +}; -export const SettingsAccountsMessageChannelsContainer = () => { - const activeTabId = useAtomComponentStateValue( - activeTabIdComponentState, +export const SettingsAccountsMessageChannelsContainer = ({ + messageChannels, +}: SettingsAccountsMessageChannelsContainerProps) => { + const activeTabId = useSettingsActiveTabId( SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID, + messageChannels.map((channel) => channel.id), ); - const setSettingsAccountsSelectedMessageChannel = useSetAtomState( - settingsAccountsSelectedMessageChannelState, - ); - - const { channels: allMessageChannels } = useMyMessageChannels(); - - const messageChannels = allMessageChannels.filter( - (channel) => - channel.isSyncEnabled && - channel.syncStage !== MessageChannelSyncStage.PENDING_CONFIGURATION && - channel.type !== MessageChannelType.EMAIL_GROUP, - ); - - const tabs = messageChannels.map((messageChannel) => ({ - id: messageChannel.id, - title: messageChannel.handle, - })); - - const handleTabChange = useCallback( - (tabId: string) => { - const selectedMessageChannel = messageChannels.find( - (channel) => channel.id === tabId, - ); - if (isDefined(selectedMessageChannel)) { - setSettingsAccountsSelectedMessageChannel(selectedMessageChannel); - } - }, - [messageChannels, setSettingsAccountsSelectedMessageChannel], - ); - - if (!messageChannels.length) { - return ; - } return ( <> - {tabs.length > 1 && ( - - - - )} {messageChannels.map((messageChannel) => ( - {(messageChannels.length === 1 || - messageChannel.id === activeTabId) && ( + {messageChannel.id === activeTabId && ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewEmailGroupChannel.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewEmailGroupChannel.tsx index fa07c2abc00a0..66395637af58d 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewEmailGroupChannel.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewEmailGroupChannel.tsx @@ -12,7 +12,7 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; export const SettingsAccountsNewEmailGroupChannel = () => { @@ -45,12 +45,12 @@ export const SettingsAccountsNewEmailGroupChannel = () => { }, [createEmailGroupChannel, handle, navigate, enqueueErrorSnackBar, t]); return ( - { /> - + ); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewImapSmtpCaldavConnection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewImapSmtpCaldavConnection.tsx index d8e1c23e73e5a..bda25b5aef660 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewImapSmtpCaldavConnection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsNewImapSmtpCaldavConnection.tsx @@ -3,7 +3,7 @@ import { FormProvider } from 'react-hook-form'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; @@ -30,7 +30,7 @@ export const SettingsAccountsNewImapSmtpCaldavConnection = () => { return ( // oxlint-disable-next-line react/jsx-props-no-spreading - { - + ); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect.tsx index 023b41169bcea..040b7fac18760 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect.tsx @@ -1,8 +1,7 @@ import { type MessageChannel } from '@/accounts/types/MessageChannel'; import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState'; -import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; -import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { useEffect } from 'react'; @@ -13,9 +12,9 @@ type SettingsAccountsSelectedMessageChannelEffectProps = { export const SettingsAccountsSelectedMessageChannelEffect = ({ messageChannels, }: SettingsAccountsSelectedMessageChannelEffectProps) => { - const activeTabId = useAtomComponentStateValue( - activeTabIdComponentState, + const activeTabId = useSettingsActiveTabId( SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID, + messageChannels.map((channel) => channel.id), ); const setSettingsAccountsSelectedMessageChannel = useSetAtomState( @@ -27,13 +26,13 @@ export const SettingsAccountsSelectedMessageChannelEffect = ({ return; } - const currentSelectionStillExists = activeTabId - ? messageChannels.some((channel) => channel.id === activeTabId) - : false; + const activeChannel = messageChannels.find( + (channel) => channel.id === activeTabId, + ); - if (!currentSelectionStillExists) { - setSettingsAccountsSelectedMessageChannel(messageChannels[0]); - } + setSettingsAccountsSelectedMessageChannel( + activeChannel ?? messageChannels[0], + ); }, [messageChannels, activeTabId, setSettingsAccountsSelectedMessageChannel]); return null; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx index e25f724a425e6..cbb2b4b5acad9 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx @@ -8,6 +8,8 @@ import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +const SETTINGS_CONTENT_MAX_WIDTH = 760; + const StyledSettingsPageContainer = styled.div<{ width?: number; isMobile?: boolean; @@ -16,6 +18,8 @@ const StyledSettingsPageContainer = styled.div<{ display: flex; flex-direction: column; gap: ${themeCssVariables.spacing[8]}; + margin: 0 auto; + max-width: ${SETTINGS_CONTENT_MAX_WIDTH}px; overflow: auto; padding: ${themeCssVariables.spacing[6]} ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]}; diff --git a/packages/twenty-front/src/modules/settings/components/layout/SettingsPageHeader.tsx b/packages/twenty-front/src/modules/settings/components/layout/SettingsPageHeader.tsx new file mode 100644 index 0000000000000..99485695259ed --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/layout/SettingsPageHeader.tsx @@ -0,0 +1,87 @@ +import { useNavigationDrawerExpanded } from '@/navigation/hooks/useNavigationDrawerExpanded'; +import { SIDE_PANEL_TOP_BAR_HEIGHT } from '@/side-panel/constants/SidePanelTopBarHeight'; +import { + Breadcrumb, + type BreadcrumbProps, +} from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { styled } from '@linaria/react'; +import { type ReactNode } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +type SettingsPageHeaderProps = { + links: BreadcrumbProps['links']; + title?: ReactNode; + tag?: ReactNode; + actionButton?: ReactNode; +}; + +// minmax(0, 1fr) side tracks (not 1fr) let a long breadcrumb truncate instead of +// pushing the centered title off its shared axis with the tabs and body. +const StyledHeader = styled.div` + align-items: center; + background-color: ${themeCssVariables.background.secondary}; + border-bottom: 1px solid ${themeCssVariables.border.color.medium}; + box-sizing: border-box; + display: grid; + gap: ${themeCssVariables.spacing[2]}; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + min-height: ${SIDE_PANEL_TOP_BAR_HEIGHT}px; + padding: 0 ${themeCssVariables.spacing[3]}; + width: 100%; +`; + +const StyledLeft = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[1]}; + min-width: 0; + overflow: hidden; +`; + +const StyledTitle = styled.div` + align-items: center; + color: ${themeCssVariables.font.color.primary}; + display: flex; + font-size: ${themeCssVariables.font.size.md}; + font-weight: ${themeCssVariables.font.weight.semiBold}; + gap: ${themeCssVariables.spacing[2]}; + min-width: 0; + text-align: center; +`; + +const StyledRight = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; + justify-content: flex-end; + min-width: 0; +`; + +export const SettingsPageHeader = ({ + links, + title, + tag, + actionButton, +}: SettingsPageHeaderProps) => { + const isMobile = useIsMobile(); + const isNavigationDrawerExpanded = useNavigationDrawerExpanded(); + + return ( + + + {!isNavigationDrawerExpanded && ( + + )} + + + + {!isMobile && isDefined(title) && title} + {!isMobile && tag} + + {actionButton} + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/components/layout/SettingsPageLayout.tsx b/packages/twenty-front/src/modules/settings/components/layout/SettingsPageLayout.tsx new file mode 100644 index 0000000000000..11fd2d3aebe26 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/layout/SettingsPageLayout.tsx @@ -0,0 +1,95 @@ +import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile'; +import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys'; +import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper'; +import { SettingsPageHeader } from '@/settings/components/layout/SettingsPageHeader'; +import { SettingsSecondaryBar } from '@/settings/components/layout/SettingsSecondaryBar'; +import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop'; +import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { styled } from '@linaria/react'; +import { type JSX, type ReactNode } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +type SettingsPageLayoutProps = { + links: BreadcrumbProps['links']; + title?: ReactNode; + actionButton?: ReactNode; + secondaryBar?: ReactNode; + children: ReactNode; + tag?: JSX.Element; +}; + +const StyledRoot = styled.div<{ isMobile: boolean }>` + display: flex; + flex: 1; + flex-direction: row; + min-height: 0; + min-width: 0; + padding: ${({ isMobile }) => + isMobile ? themeCssVariables.spacing[1] : themeCssVariables.spacing[2]}; +`; + +const StyledMainCardWrapper = styled.div` + display: flex; + flex: 1 1 0; + min-width: 0; + width: 0; +`; + +const StyledCard = styled.div` + background: ${themeCssVariables.background.primary}; + border: 1px solid ${themeCssVariables.border.color.medium}; + border-radius: ${themeCssVariables.border.radius.md}; + box-sizing: border-box; + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + overflow: hidden; + width: 100%; +`; + +const StyledBodyContent = styled.div` + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + width: 100%; +`; + +export const SettingsPageLayout = ({ + links, + title, + actionButton, + secondaryBar, + children, + tag, +}: SettingsPageLayoutProps) => { + const isMobile = useIsMobile(); + + useCommandMenuHotKeys(); + + return ( + + + + + {isDefined(secondaryBar) && ( + {secondaryBar} + )} + + + {children} + + + + {isMobile ? : } + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/components/layout/SettingsSecondaryBar.tsx b/packages/twenty-front/src/modules/settings/components/layout/SettingsSecondaryBar.tsx new file mode 100644 index 0000000000000..027edd9e90fd7 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/layout/SettingsSecondaryBar.tsx @@ -0,0 +1,29 @@ +import { styled } from '@linaria/react'; +import { type ReactNode } from 'react'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +// Bottom separator is an ::after (not border-bottom), and the bar keeps full +// height (default align-items) so the active tab's underline lands exactly on it. +const StyledSecondaryBar = styled.div` + box-sizing: border-box; + display: flex; + flex-shrink: 0; + min-height: ${themeCssVariables.spacing[10]}; + padding: 0 ${themeCssVariables.spacing[3]}; + position: relative; + width: 100%; + + &::after { + background-color: ${themeCssVariables.border.color.light}; + bottom: 0; + content: ''; + height: 1px; + left: 0; + position: absolute; + right: 0; + } +`; + +export const SettingsSecondaryBar = ({ children }: { children: ReactNode }) => ( + {children} +); diff --git a/packages/twenty-front/src/modules/settings/components/layout/SettingsTabBar.tsx b/packages/twenty-front/src/modules/settings/components/layout/SettingsTabBar.tsx new file mode 100644 index 0000000000000..3bb97f1d1ca19 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/layout/SettingsTabBar.tsx @@ -0,0 +1,64 @@ +import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId'; +import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/TabListFromUrlOptionalEffect'; +import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap'; +import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; +import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; +import { styled } from '@linaria/react'; +import { TabButton } from 'twenty-ui/input'; + +type SettingsTabBarProps = { + tabs: SingleTabProps[]; + componentInstanceId: string; +}; + +const StyledTabBar = styled.div` + display: flex; + flex: 1; + gap: ${TAB_LIST_GAP}px; + justify-content: center; + min-width: 0; +`; + +export const SettingsTabBar = ({ + tabs, + componentInstanceId, +}: SettingsTabBarProps) => { + const visibleTabs = tabs.filter((tab) => !tab.hide); + const visibleTabIds = visibleTabs.map((tab) => tab.id); + + const activeTabId = useSettingsActiveTabId( + componentInstanceId, + visibleTabIds, + ); + + if (visibleTabs.length === 0) { + return null; + } + + return ( + + + + {visibleTabs.map((tab) => ( + + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/components/layout/SettingsWizardStepBar.tsx b/packages/twenty-front/src/modules/settings/components/layout/SettingsWizardStepBar.tsx new file mode 100644 index 0000000000000..7d503a756ff7c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/layout/SettingsWizardStepBar.tsx @@ -0,0 +1,63 @@ +import { styled } from '@linaria/react'; +import { type ReactNode } from 'react'; +import { IconChevronLeft } from 'twenty-ui/display'; +import { LightIconButton } from 'twenty-ui/input'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +type SettingsWizardStepBarProps = { + label: ReactNode; + onBack?: () => void; + trailing?: ReactNode; +}; + +const StyledStepBar = styled.div` + align-items: center; + display: grid; + flex: 1; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + min-width: 0; +`; + +const StyledLeft = styled.div` + align-items: center; + display: flex; + min-width: 0; +`; + +const StyledLabel = styled.div` + color: ${themeCssVariables.font.color.primary}; + font-size: ${themeCssVariables.font.size.md}; + font-weight: ${themeCssVariables.font.weight.semiBold}; + justify-self: center; + min-width: 0; + text-align: center; +`; + +const StyledRight = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; + justify-content: flex-end; + min-width: 0; +`; + +export const SettingsWizardStepBar = ({ + label, + onBack, + trailing, +}: SettingsWizardStepBarProps) => ( + + + {onBack && ( + + )} + + {label} + {trailing} + +); diff --git a/packages/twenty-front/src/modules/settings/components/layout/useSettingsActiveTabId.ts b/packages/twenty-front/src/modules/settings/components/layout/useSettingsActiveTabId.ts new file mode 100644 index 0000000000000..9649ec71ef4bb --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/layout/useSettingsActiveTabId.ts @@ -0,0 +1,29 @@ +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useLocation } from 'react-router-dom'; +import { isDefined } from 'twenty-shared/utils'; + +// Resolves the active tab synchronously so the content never renders a null/blank +// frame before the effects settle: the URL hash if it points to a tab (so deep-links +// win), else the stored tab if still valid, else the first tab. +export const useSettingsActiveTabId = ( + componentInstanceId: string, + tabIds: string[], +): string | null => { + const activeTabId = useAtomComponentStateValue( + activeTabIdComponentState, + componentInstanceId, + ); + const { hash } = useLocation(); + + const hashTabId = hash.replace('#', ''); + if (tabIds.includes(hashTabId)) { + return hashTabId; + } + + if (isDefined(activeTabId) && tabIds.includes(activeTabId)) { + return activeTabId; + } + + return tabIds[0] ?? null; +}; diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx index d955f1f52a014..de3a460ef5938 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx @@ -9,7 +9,7 @@ import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { TextArea } from '@/ui/input/components/TextArea'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; import { Trans, useLingui } from '@lingui/react/macro'; import { SettingsPath } from 'twenty-shared/types'; import { @@ -72,13 +72,12 @@ export const SettingsDevelopersWebhookForm = ({ return ( // oxlint-disable-next-line react/jsx-props-no-spreading - )} - + {!isCreationMode && ( { }; return ( - Workspace, - href: getSettingsPath(SettingsPath.Workspace), + href: getSettingsPath(SettingsPath.General), }, { children: Apps, @@ -286,6 +286,6 @@ export const SettingPublicDomain = () => { /> - + ); }; diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx index 94bcefaffa600..6b4486054dd84 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx @@ -7,7 +7,7 @@ import { SettingsDomainRecords } from '@/settings/domains/components/SettingsDom import { useSettingsCustomDomain } from '@/settings/domains/hooks/useSettingsCustomDomain'; import { customDomainRecordsState } from '@/settings/domains/states/customDomainRecordsState'; import { TextInput } from '@/ui/input/components/TextInput'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { Trans, useLingui } from '@lingui/react/macro'; import { styled } from '@linaria/react'; @@ -62,22 +62,22 @@ export const SettingsCustomDomain = () => { } = useSettingsCustomDomain(); return ( - Workspace, - href: getSettingsPath(SettingsPath.Workspace), + href: getSettingsPath(SettingsPath.General), }, { children: General, - href: getSettingsPath(SettingsPath.Workspace), + href: getSettingsPath(SettingsPath.General), }, { children: Custom Domain }, ]} actionButton={ navigate(SettingsPath.Workspace)} + onCancel={() => navigate(SettingsPath.General)} isSaveDisabled={isSaveDisabled} isLoading={isSubmitting} onSave={handleSave} @@ -133,6 +133,6 @@ export const SettingsCustomDomain = () => { )} - + ); }; diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingsSubdomain.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingsSubdomain.tsx index 722e15071fa24..e0f7fed6bd3eb 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingsSubdomain.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingsSubdomain.tsx @@ -8,7 +8,7 @@ import { } from '@/settings/domains/hooks/useSettingsSubdomain'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { Trans, useLingui } from '@lingui/react/macro'; import { styled } from '@linaria/react'; @@ -41,22 +41,22 @@ export const SettingsSubdomain = () => { return ( <> - Workspace, - href: getSettingsPath(SettingsPath.Workspace), + href: getSettingsPath(SettingsPath.General), }, { children: General, - href: getSettingsPath(SettingsPath.Workspace), + href: getSettingsPath(SettingsPath.General), }, { children: Subdomain }, ]} actionButton={ navigate(SettingsPath.Workspace)} + onCancel={() => navigate(SettingsPath.General)} isSaveDisabled={isSaveDisabled} isLoading={isSubmitting} onSave={handleSave} @@ -86,7 +86,7 @@ export const SettingsSubdomain = () => { - + { items: [ { label: t`General`, - path: SettingsPath.Workspace, + path: SettingsPath.General, Icon: IconSettings, isHidden: !permissionMap[PermissionFlagType.WORKSPACE], }, @@ -168,14 +166,12 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { path: SettingsPath.Applications, Icon: IconPlug, isHidden: !permissionMap[PermissionFlagType.APPLICATIONS], - modifier: 'new', }, { label: t`AI`, path: SettingsPath.AI, Icon: IconSparkles, isHidden: !permissionMap[PermissionFlagType.AI], - modifier: 'new', }, { label: t`Email`, @@ -185,13 +181,6 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { !isEmailGroupFeatureEnabled || !permissionMap[PermissionFlagType.WORKSPACE], }, - { - label: t`Security`, - path: SettingsPath.Security, - Icon: IconKey, - isAdvanced: true, - isHidden: !permissionMap[PermissionFlagType.SECURITY], - }, ], }, { @@ -204,9 +193,9 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { isHidden: !isAdminEnabled, }, { - label: t`Updates`, - path: SettingsPath.Updates, - Icon: IconRocket, + label: t`Community`, + path: SettingsPath.Community, + Icon: IconUsers, isHidden: !permissionMap[PermissionFlagType.WORKSPACE], }, { diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx index 80c5b603939e2..2d13312594384 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx @@ -6,10 +6,11 @@ import { SettingsRolePermissionsObjectLevelObjectFieldPermissionTable } from '@/ import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel'; import { SettingsRolePermissionsObjectLevelRecordLevelSection } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelSection'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; +import { SettingsWizardStepBar } from '@/settings/components/layout/SettingsWizardStepBar'; import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; import { t } from '@lingui/core/macro'; -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { SettingsPath } from 'twenty-shared/types'; import { @@ -35,6 +36,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ objectMetadataId, }: SettingsRolePermissionsObjectLevelObjectFormProps) => { const [searchParams] = useSearchParams(); + const navigate = useNavigate(); const fromAgentId = searchParams.get('fromAgent'); const currentWorkspace = useAtomStateValue(currentWorkspaceState); @@ -74,7 +76,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ ? [ { children: t`Workspace`, - href: getSettingsPath(SettingsPath.Workspace), + href: getSettingsPath(SettingsPath.General), }, { children: t`AI`, @@ -93,7 +95,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ : [ { children: t`Workspace`, - href: getSettingsPath(SettingsPath.Workspace), + href: getSettingsPath(SettingsPath.General), }, { children: t`Members`, @@ -119,6 +121,13 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ ? getSettingsPath(SettingsPath.AiAgentDetail, { agentId: agent.id }) : getSettingsPath(SettingsPath.RoleDetail, { roleId }); + const previousStepPath = `${getSettingsPath(SettingsPath.RoleAddObjectLevel, { + roleId, + })}${fromAgentId ? `?fromAgent=${fromAgentId}` : ''}`; + + const headerTitle = + fromAgentId && isDefined(agent) ? agent.label : settingsDraftRole.label; + const objectPredicates = settingsDraftRole.rowLevelPermissionPredicates?.filter( (predicate) => predicate.objectMetadataId === objectMetadataItem.id, @@ -138,17 +147,23 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ const isFinishDisabled = hasInvalidPredicate; return ( - navigate(previousStepPath)} + trailing={ +