feat(twenty-front): generalize the page primary/secondary bars (flat redesign) (#21308)

Replaces #21279 and #21282 with one clean PR from `main`.

Generalizes the settings primary-bar / secondary-bar card chrome to the
record index, record show and standalone pages via a shared
`PageCardLayout` + `PageCardHeader` (the side panel sits as a sibling of
the content card), and applies the new flat design direction: square
corners on the card, side panel and loading skeletons.

Iterating toward the new design (Figma node 102282-221623); the
confirmed direction and the explicit "remove rounded corners" change are
in, remaining designer specifics to follow.
This commit is contained in:
Félix Malfait
2026-06-08 22:47:05 +02:00
committed by GitHub
parent a48c158a66
commit bfefcd3755
29 changed files with 528 additions and 517 deletions

View File

@@ -1,59 +1,58 @@
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { type ReactNode, useContext } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import {
ThemeContext,
themeCssVariables,
} from 'twenty-ui-deprecated/theme-constants';
const StyledHeaderSkeleton = styled.div`
align-items: center;
background: ${themeCssVariables.background.noisy};
const StyledBody = styled.div`
display: flex;
flex-direction: row;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
justify-content: space-between;
min-height: ${PAGE_BAR_MIN_HEIGHT}px;
padding: ${themeCssVariables.spacing[3]};
`;
const StyledHeaderLeft = styled.div`
flex: 1;
`;
type PageContentSkeletonLoaderProps = {
secondaryBar?: ReactNode;
};
export const PageContentSkeletonLoader = () => {
export const PageContentSkeletonLoader = ({
secondaryBar,
}: PageContentSkeletonLoaderProps) => {
const { theme } = useContext(ThemeContext);
return (
<>
<StyledHeaderSkeleton>
<StyledHeaderLeft>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
width={104}
/>
</SkeletonTheme>
</StyledHeaderLeft>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
width={132}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<PageCardLayout
header={
<PageCardHeader
icon={<Skeleton width={20} height={20} />}
title={
<Skeleton
width={120}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
}
/>
</SkeletonTheme>
</StyledHeaderSkeleton>
<PageBody>{null}</PageBody>
</>
}
secondaryBar={secondaryBar}
showInformationBanner={false}
>
<StyledBody>
<Skeleton
count={8}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
/>
</StyledBody>
</PageCardLayout>
</SkeletonTheme>
);
};

View File

@@ -1,14 +0,0 @@
import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader';
import { styled } from '@linaria/react';
const StyledRightPanelContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
export const RightPanelSkeletonLoader = () => (
<StyledRightPanelContainer>
<PageContentSkeletonLoader />
</StyledRightPanelContainer>
);

View File

@@ -9,7 +9,7 @@ import {
} from 'twenty-ui-deprecated/theme-constants';
import { ModalBackdrop } from 'twenty-ui-deprecated/layout';
import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader';
import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader';
const StyledContainer = styled.div`
background: ${themeCssVariables.background.noisy};
@@ -44,7 +44,7 @@ export const UserOrMetadataLoader = () => {
<StyledLeftPanelWrapper>
<LeftPanelSkeletonLoader />
</StyledLeftPanelWrapper>
<RightPanelSkeletonLoader />
<PageContentSkeletonLoader />
</StyledContainer>
);
};

View File

@@ -1,4 +1,3 @@
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { type ReactNode, Suspense } from 'react';
import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader';
@@ -7,13 +6,7 @@ type LazyRouteProps = {
fallback?: ReactNode;
};
const LazyRouteFallback = () => (
<PageContainer>
<PageContentSkeletonLoader />
</PageContainer>
);
export const LazyRoute = ({
children,
fallback = <LazyRouteFallback />,
fallback = <PageContentSkeletonLoader />,
}: LazyRouteProps) => <Suspense fallback={fallback}>{children}</Suspense>;

View File

@@ -5,8 +5,10 @@ import { VerifyLoginTokenEffect } from '@/auth/components/VerifyLoginTokenEffect
import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect';
import indexAppPath from '@/navigation/utils/indexAppPath';
import { RecordIndexSkeletonLoader } from '@/object-record/record-index/components/RecordIndexSkeletonLoader';
import { BlankLayout } from '@/ui/layout/page/components/BlankLayout';
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
import { MainAppLayoutWithSidePanel } from '@/ui/layout/page/components/MainAppLayoutWithSidePanel';
import { AppPath } from 'twenty-shared/types';
import { lazy } from 'react';
@@ -209,48 +211,50 @@ export const useCreateAppRouter = (
</LazyRoute>
}
/>
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
<Route
path={AppPath.RecordIndexPage}
element={
<LazyRoute>
<RecordIndexPage />
</LazyRoute>
}
/>
<Route
path={AppPath.RecordShowPage}
element={
<LazyRoute>
<RecordShowPage />
</LazyRoute>
}
/>
<Route
path={AppPath.PageLayoutPage}
element={
<LazyRoute>
<StandalonePageLayoutPage />
</LazyRoute>
}
/>
<Route
path={AppPath.SettingsCatchAll}
element={
<SettingsRoutes
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
isAdminPageEnabled={isAdminPageEnabled}
/>
}
/>
<Route
path={AppPath.NotFoundWildcard}
element={
<LazyRoute>
<NotFound />
</LazyRoute>
}
/>
<Route element={<MainAppLayoutWithSidePanel />}>
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
<Route
path={AppPath.RecordIndexPage}
element={
<LazyRoute fallback={<RecordIndexSkeletonLoader />}>
<RecordIndexPage />
</LazyRoute>
}
/>
<Route
path={AppPath.RecordShowPage}
element={
<LazyRoute>
<RecordShowPage />
</LazyRoute>
}
/>
<Route
path={AppPath.PageLayoutPage}
element={
<LazyRoute>
<StandalonePageLayoutPage />
</LazyRoute>
}
/>
<Route
path={AppPath.SettingsCatchAll}
element={
<SettingsRoutes
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
isAdminPageEnabled={isAdminPageEnabled}
/>
}
/>
<Route
path={AppPath.NotFoundWildcard}
element={
<LazyRoute>
<NotFound />
</LazyRoute>
}
/>
</Route>
</Route>
<Route element={<BlankLayout />}>
<Route

View File

@@ -31,12 +31,14 @@ const StyledPreviewWrapper = styled.div`
type CommandMenuItemRendererProps = {
item: CommandMenuItemFieldsFragment;
isPrimaryAction?: boolean;
};
type CommandMenuItemButtonRendererProps = CommandMenuItemRendererProps;
const CommandMenuItemButtonRenderer = ({
item,
isPrimaryAction = false,
}: CommandMenuItemButtonRendererProps) => {
const { commandMenuContextApi, isInPreviewMode } =
useContext(CommandMenuContext);
@@ -60,7 +62,10 @@ const CommandMenuItemButtonRenderer = ({
if (isInPreviewMode) {
return (
<StyledPreviewWrapper>
<CommandMenuButton command={command} />
<CommandMenuButton
command={command}
isPrimaryAction={isPrimaryAction}
/>
</StyledPreviewWrapper>
);
}
@@ -70,6 +75,7 @@ const CommandMenuItemButtonRenderer = ({
command={command}
onClick={disabled ? undefined : handleClick}
disabled={disabled}
isPrimaryAction={isPrimaryAction}
/>
);
};
@@ -167,11 +173,17 @@ const CommandMenuItemSelectableRenderer = ({
// oxlint-disable-next-line twenty/effect-components
export const CommandMenuItemRenderer = ({
item,
isPrimaryAction,
}: CommandMenuItemRendererProps) => {
const { displayType } = useContext(CommandMenuContext);
if (displayType === 'button') {
return <CommandMenuItemButtonRenderer item={item} />;
return (
<CommandMenuItemButtonRenderer
item={item}
isPrimaryAction={isPrimaryAction}
/>
);
}
if (displayType === 'listItem' || displayType === 'dropdownItem') {

View File

@@ -8,6 +8,7 @@ import { styled } from '@linaria/react';
import { motion } from 'framer-motion';
import { useContext, useMemo } from 'react';
import { ThemeContext } from 'twenty-ui-deprecated/theme-constants';
import { EngineComponentKey } from '~/generated-metadata/graphql';
const StyledCommandMenuItemContainer = styled(motion.div)`
align-items: center;
@@ -80,7 +81,13 @@ export const PinnedCommandMenuItemButtons = () => {
ease: 'easeInOut',
}}
>
<CommandMenuItemRenderer item={item} />
<CommandMenuItemRenderer
item={item}
isPrimaryAction={
item.engineComponentKey ===
EngineComponentKey.CREATE_NEW_RECORD
}
/>
</StyledCommandMenuItemContainer>
))}
</StyledItemsContainer>

View File

@@ -28,6 +28,7 @@ export type CommandMenuButtonProps = {
onClick?: (event?: MouseEvent<HTMLElement>) => void;
to?: string;
disabled?: boolean;
isPrimaryAction?: boolean;
};
export const CommandMenuButton = ({
@@ -35,6 +36,7 @@ export const CommandMenuButton = ({
onClick,
to,
disabled = false,
isPrimaryAction = false,
}: CommandMenuButtonProps) => {
const resolvedLabel = getCommandMenuItemLabel(command.label);
@@ -50,8 +52,8 @@ export const CommandMenuButton = ({
<Button
Icon={command.Icon}
size="small"
variant="secondary"
accent={buttonAccent}
variant={isPrimaryAction ? 'primary' : 'secondary'}
accent={isPrimaryAction ? 'blue' : buttonAccent}
to={to}
onClick={onClick}
disabled={disabled}
@@ -63,8 +65,8 @@ export const CommandMenuButton = ({
<IconButton
Icon={command.Icon}
size="small"
variant="secondary"
accent={buttonAccent}
variant={isPrimaryAction ? 'primary' : 'secondary'}
accent={isPrimaryAction ? 'blue' : buttonAccent}
to={to}
onClick={onClick}
disabled={disabled}

View File

@@ -1,83 +0,0 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { useIsMobile } from 'twenty-ui-deprecated/utilities';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
type MainContainerLayoutWithSidePanelProps = {
children: ReactNode;
};
const StyledMainContainerLayoutForDesktop = styled.div`
display: flex;
flex: 1;
min-height: 0;
padding-bottom: ${themeCssVariables.spacing[3]};
padding-right: ${themeCssVariables.spacing[3]};
`;
const StyledPageBodyForDesktopContainer = styled.div`
display: flex;
flex: 1 1 0;
flex-direction: column;
min-width: 0;
width: 0;
> * {
padding-bottom: 0;
padding-right: 0;
}
`;
const StyledMainContainerLayoutForMobile = styled.div`
display: flex;
flex: 1;
min-height: 0;
padding: 0;
`;
const StyledPageBodyForMobileContainer = styled.div`
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
min-width: 0;
width: 0;
> * {
padding-bottom: 0;
padding-left: ${themeCssVariables.spacing[1]};
padding-right: ${themeCssVariables.spacing['1.5']};
}
`;
export const MainContainerLayoutWithSidePanel = ({
children,
}: MainContainerLayoutWithSidePanelProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
if (isMobile) {
return (
<StyledMainContainerLayoutForMobile>
<StyledPageBodyForMobileContainer>
<PageBody>{children}</PageBody>
</StyledPageBodyForMobileContainer>
<CommandMenuForMobile />
</StyledMainContainerLayoutForMobile>
);
}
return (
<StyledMainContainerLayoutForDesktop>
<StyledPageBodyForDesktopContainer>
<PageBody>{children}</PageBody>
</StyledPageBodyForDesktopContainer>
<SidePanelForDesktop />
</StyledMainContainerLayoutForDesktop>
);
};

View File

@@ -1,21 +1,16 @@
import { styled } from '@linaria/react';
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
import { RecordBoardContainer } from '@/object-record/record-board/components/RecordBoardContainer';
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexCalendarContainer } from '@/object-record/record-index/components/RecordIndexCalendarContainer';
import { RecordIndexEmptyStateNotShared } from '@/object-record/record-index/components/RecordIndexEmptyStateNotShared';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { useHasCurrentViewNonReadableFields } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
@@ -24,7 +19,6 @@ const StyledContainer = styled.div`
flex-direction: column;
height: 100%;
overflow: hidden;
width: 100%;
`;
@@ -38,67 +32,43 @@ const StyledContainerWithPadding = styled.div`
export const RecordIndexContainer = () => {
const recordIndexViewType = useAtomStateValue(recordIndexViewTypeState);
const {
objectNamePlural,
recordIndexId,
objectMetadataItem,
objectNameSingular,
} = useRecordIndexContextOrThrow();
const { recordIndexId, objectMetadataItem, objectNameSingular } =
useRecordIndexContextOrThrow();
const { hasCurrentViewNonReadableFields, nonReadableViewFieldInfo } =
useHasCurrentViewNonReadableFields(objectMetadataItem);
return (
<>
<StyledContainer>
<InformationBannerWrapper />
<SpreadsheetImportProvider>
<ViewBar
isReadOnly={hasCurrentViewNonReadableFields}
viewBarId={recordIndexId}
optionsDropdownButton={
<ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.TABLE}
<StyledContainer>
{hasCurrentViewNonReadableFields ? (
<RecordIndexEmptyStateNotShared
nonReadableViewFieldInfo={nonReadableViewFieldInfo}
/>
) : (
<>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.TABLE && (
<RecordIndexTableContainer recordTableId={recordIndexId} />
)}
{recordIndexViewType === ViewType.KANBAN && (
<StyledContainerWithPadding>
<RecordBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
{hasCurrentViewNonReadableFields ? (
<RecordIndexEmptyStateNotShared
nonReadableViewFieldInfo={nonReadableViewFieldInfo}
/>
) : (
<>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.TABLE && (
<RecordIndexTableContainer recordTableId={recordIndexId} />
)}
{recordIndexViewType === ViewType.KANBAN && (
<StyledContainerWithPadding>
<RecordBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
</StyledContainerWithPadding>
)}
{recordIndexViewType === ViewType.CALENDAR && (
<StyledContainerWithPadding>
<RecordIndexCalendarContainer
recordCalendarInstanceId={recordIndexId}
viewBarInstanceId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</>
)}
</StyledContainer>
</>
</StyledContainerWithPadding>
)}
{recordIndexViewType === ViewType.CALENDAR && (
<StyledContainerWithPadding>
<RecordIndexCalendarContainer
recordCalendarInstanceId={recordIndexId}
viewBarInstanceId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</>
)}
</StyledContainer>
);
};

View File

@@ -3,7 +3,7 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { getCommandMenuIdFromRecordIndexId } from '@/command-menu-item/utils/getCommandMenuIdFromRecordIndexId';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { RecordIndexViewBar } from '@/object-record/record-index/components/RecordIndexViewBar';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
@@ -16,6 +16,7 @@ import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hook
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { styled } from '@linaria/react';
@@ -91,8 +92,12 @@ export const RecordIndexContainerGater = () => {
}}
>
<PageTitle title={objectMetadataItem.labelPlural} />
<RecordIndexPageHeader />
<MainContainerLayoutWithSidePanel>
<PageCardLayout
header={<RecordIndexPageHeader />}
secondaryBar={
hasObjectReadPermissions && <RecordIndexViewBar />
}
>
<StyledIndexContainer
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
>
@@ -105,7 +110,7 @@ export const RecordIndexContainerGater = () => {
<RecordIndexEmptyStateNotShared />
)}
</StyledIndexContainer>
</MainContainerLayoutWithSidePanel>
</PageCardLayout>
</CommandMenuComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
<RecordIndexLoadBaseOnContextStoreEffect />

View File

@@ -1,5 +1,4 @@
import { RecordIndexCommandMenu } from '@/command-menu-item/components/RecordIndexCommandMenu';
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
@@ -7,7 +6,8 @@ import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/st
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { RecordIndexPageHeaderIcon } from '@/object-record/record-index/components/RecordIndexPageHeaderIcon';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
@@ -68,18 +68,19 @@ export const RecordIndexPageHeader = () => {
);
return (
<PageHeader
title={pageHeaderTitle}
Icon={() => (
<PageCardHeader
icon={
<RecordIndexPageHeaderIcon objectMetadataItem={objectMetadataItem} />
)}
>
{isDefined(contextStoreCurrentViewId) && (
<>
<RecordIndexCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</>
)}
</PageHeader>
}
title={pageHeaderTitle}
actionButton={
isDefined(contextStoreCurrentViewId) ? (
<>
<RecordIndexCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</>
) : undefined
}
/>
);
};

View File

@@ -1,8 +1,33 @@
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { styled } from '@linaria/react';
import Skeleton from 'react-loading-skeleton';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader';
const StyledSecondaryBar = styled.div`
align-items: center;
border-bottom: 1px solid ${themeCssVariables.border.color.light};
box-sizing: border-box;
display: flex;
flex-shrink: 0;
justify-content: space-between;
min-height: ${themeCssVariables.spacing[10]};
padding: 0 ${themeCssVariables.spacing[3]};
`;
export const RecordIndexSkeletonLoader = () => (
<PageContainer>
<PageContentSkeletonLoader />
</PageContainer>
<PageContentSkeletonLoader
secondaryBar={
<StyledSecondaryBar>
<Skeleton
width={120}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
<Skeleton
width={180}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
</StyledSecondaryBar>
}
/>
);

View File

@@ -0,0 +1,39 @@
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHasCurrentViewNonReadableFields } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
export const RecordIndexViewBar = () => {
const recordIndexViewType = useAtomStateValue(recordIndexViewTypeState);
const { objectNamePlural, recordIndexId, objectMetadataItem } =
useRecordIndexContextOrThrow();
const { hasCurrentViewNonReadableFields } =
useHasCurrentViewNonReadableFields(objectMetadataItem);
return (
<SpreadsheetImportProvider>
<ViewBar
isReadOnly={hasCurrentViewNonReadableFields}
viewBarId={recordIndexId}
optionsDropdownButton={
<ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.TABLE}
/>
}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
);
};

View File

@@ -1,51 +1,24 @@
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { SettingsPageHeader } from '@/settings/components/layout/SettingsPageHeader';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { styled } from '@linaria/react';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { useContext } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import {
ThemeContext,
themeCssVariables,
} from 'twenty-ui-deprecated/theme-constants';
const StyledRoot = styled.div<{ isMobile: boolean }>`
display: flex;
flex: 1;
min-height: 0;
min-width: 0;
padding: ${({ isMobile }) =>
isMobile ? themeCssVariables.spacing[1] : themeCssVariables.spacing[2]};
`;
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%;
`;
import { ThemeContext } from 'twenty-ui-deprecated/theme-constants';
export const SettingsSkeletonLoader = () => {
const isMobile = useIsMobile();
const { theme } = useContext(ThemeContext);
return (
<StyledRoot isMobile={isMobile}>
<StyledCard>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<SettingsPageHeader
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<PageCardLayout
header={
<PageCardHeader
links={[
{
children: (
@@ -63,11 +36,13 @@ export const SettingsSkeletonLoader = () => {
/>
}
/>
<SettingsPageContainer>
<SettingsSectionSkeletonLoader />
</SettingsPageContainer>
</SkeletonTheme>
</StyledCard>
</StyledRoot>
}
showInformationBanner={false}
>
<SettingsPageContainer>
<SettingsSectionSkeletonLoader />
</SettingsPageContainer>
</PageCardLayout>
</SkeletonTheme>
);
};

View File

@@ -1,15 +1,9 @@
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 { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
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-deprecated/theme-constants';
type SettingsPageLayoutProps = {
links: BreadcrumbProps['links'];
@@ -20,44 +14,6 @@ type SettingsPageLayoutProps = {
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,
@@ -65,31 +21,22 @@ export const SettingsPageLayout = ({
secondaryBar,
children,
tag,
}: SettingsPageLayoutProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
return (
<StyledRoot isMobile={isMobile}>
<StyledMainCardWrapper>
<StyledCard>
<SettingsPageHeader
links={links}
title={title}
tag={tag}
actionButton={actionButton}
/>
{isDefined(secondaryBar) && (
<SettingsSecondaryBar>{secondaryBar}</SettingsSecondaryBar>
)}
<StyledBodyContent>
<InformationBannerWrapper />
{children}
</StyledBodyContent>
</StyledCard>
</StyledMainCardWrapper>
{isMobile ? <CommandMenuForMobile /> : <SidePanelForDesktop />}
</StyledRoot>
);
};
}: SettingsPageLayoutProps) => (
<PageCardLayout
header={
<PageCardHeader
links={links}
title={title}
tag={tag}
actionButton={actionButton}
/>
}
secondaryBar={
isDefined(secondaryBar) ? (
<SettingsSecondaryBar>{secondaryBar}</SettingsSecondaryBar>
) : undefined
}
>
{children}
</PageCardLayout>
);

View File

@@ -37,8 +37,6 @@ const StyledSidePanelWrapper = styled.div<{
const StyledSidePanel = styled.aside`
background: ${themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
box-sizing: border-box;
display: flex;
flex-direction: column;
@@ -58,8 +56,6 @@ const StyledModalContainer = styled.div`
z-index: 1;
`;
const GAP_WIDTH = 8;
export const SidePanelForDesktop = () => {
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
const isSidePanelClosing = useAtomStateValue(isSidePanelClosingState);
@@ -130,7 +126,7 @@ export const SidePanelForDesktop = () => {
currentWidth={sidePanelWidth}
onWidthChange={handleWidthChange}
onCollapse={handleCollapse}
gapWidth={isSidePanelOpened ? GAP_WIDTH : 0}
gapWidth={0}
cssVariableName={SIDE_PANEL_WIDTH_VAR}
onResizeStart={handleResizeStart}
/>

View File

@@ -15,10 +15,7 @@ import {
TooltipPosition,
} from 'twenty-ui-deprecated/display';
import { AnimatedButton } from 'twenty-ui-deprecated/input';
import {
getOsControlSymbol,
useIsMobile,
} from 'twenty-ui-deprecated/utilities';
import { useIsMobile } from 'twenty-ui-deprecated/utilities';
import {
ThemeContext,
themeCssVariables,
@@ -157,7 +154,6 @@ export const SidePanelToggleButton = () => {
size={isMobile ? 'medium' : 'small'}
variant="secondary"
accent="default"
hotkeys={[getOsControlSymbol(), 'K']}
ariaLabel={ariaLabel}
onClick={toggleSidePanelMenu}
animate={{

View File

@@ -11,53 +11,55 @@ import { Button, LightIconButton } from 'twenty-ui-deprecated/input';
import { BackgroundMockTable } from '@/sign-in-background-mock/components/BackgroundMockTable';
import { BackgroundMockViewBar } from '@/sign-in-background-mock/components/BackgroundMockViewBar';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
const StyledTableContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
width: 100%;
`;
export const BackgroundMockPage = () => {
return (
<PageContainer>
<PageHeader
title={t`Companies`}
Icon={() => (
<TintedIconTile Icon={IconBuildingSkyscraper} color="blue" />
)}
>
<Button
Icon={IconPlus}
title={t`New Company`}
variant="primary"
accent="default"
size="small"
<PageCardLayout
header={
<PageCardHeader
icon={<TintedIconTile Icon={IconBuildingSkyscraper} color="blue" />}
title={t`Companies`}
actionButton={
<>
<Button
Icon={IconPlus}
title={t`New Company`}
variant="primary"
accent="blue"
size="small"
/>
<LightIconButton
Icon={IconDotsVertical}
accent="tertiary"
size="small"
/>
<Button
Icon={IconLayoutSidebarRight}
variant="secondary"
accent="default"
size="small"
/>
</>
}
/>
<LightIconButton
Icon={IconDotsVertical}
accent="tertiary"
size="small"
/>
<Button
Icon={IconLayoutSidebarRight}
variant="secondary"
accent="default"
size="small"
/>
</PageHeader>
<PageBody>
<StyledTableContainer>
<BackgroundMockViewBar />
<BackgroundMockTable />
</StyledTableContainer>
</PageBody>
</PageContainer>
}
secondaryBar={<BackgroundMockViewBar />}
showInformationBanner={false}
>
<StyledTableContainer>
<BackgroundMockTable />
</StyledTableContainer>
</PageCardLayout>
);
};

View File

@@ -0,0 +1,37 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { styled } from '@linaria/react';
import { Outlet } from 'react-router-dom';
const StyledRow = styled.div`
display: flex;
flex: 1;
flex-direction: row;
min-height: 0;
min-width: 0;
`;
const StyledContent = styled.div`
display: flex;
flex: 1 1 0;
min-height: 0;
min-width: 0;
overflow: hidden;
`;
export const MainAppLayoutWithSidePanel = () => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
return (
<StyledRow>
<StyledContent>
<Outlet />
</StyledContent>
{isMobile ? <CommandMenuForMobile /> : <SidePanelForDesktop />}
</StyledRow>
);
};

View File

@@ -11,23 +11,22 @@ import { type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
type SettingsPageHeaderProps = {
links: BreadcrumbProps['links'];
type PageCardHeaderProps = {
links?: BreadcrumbProps['links'];
breadcrumb?: ReactNode;
icon?: ReactNode;
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;
display: flex;
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%;
@@ -36,7 +35,7 @@ const StyledHeader = styled.div`
const StyledLeft = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[1]};
gap: ${themeCssVariables.spacing[2]};
min-width: 0;
overflow: hidden;
`;
@@ -49,23 +48,25 @@ const StyledTitle = styled.div`
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;
flex: 1;
gap: ${themeCssVariables.spacing[2]};
justify-content: flex-end;
min-width: 0;
`;
export const SettingsPageHeader = ({
export const PageCardHeader = ({
links,
breadcrumb,
icon,
title,
tag,
actionButton,
}: SettingsPageHeaderProps) => {
}: PageCardHeaderProps) => {
const isMobile = useIsMobile();
const isNavigationDrawerExpanded = useNavigationDrawerExpanded();
@@ -75,12 +76,18 @@ export const SettingsPageHeader = ({
{!isNavigationDrawerExpanded && (
<NavigationDrawerCollapseButton direction="right" />
)}
<Breadcrumb links={links} />
{isDefined(breadcrumb)
? breadcrumb
: isDefined(links) && <Breadcrumb links={links} />}
{!isMobile &&
(isDefined(icon) || isDefined(title) || isDefined(tag)) && (
<StyledTitle>
{icon}
{isDefined(title) && title}
{tag}
</StyledTitle>
)}
</StyledLeft>
<StyledTitle>
{!isMobile && isDefined(title) && title}
{!isMobile && tag}
</StyledTitle>
<StyledRight>{actionButton}</StyledRight>
</StyledHeader>
);

View File

@@ -0,0 +1,69 @@
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
type PageCardLayoutProps = {
header: ReactNode;
secondaryBar?: ReactNode;
children: ReactNode;
showInformationBanner?: boolean;
};
const StyledRoot = styled.div`
display: flex;
flex: 1;
flex-direction: row;
min-height: 0;
min-width: 0;
`;
const StyledMainCardWrapper = styled.div`
display: flex;
flex: 1 1 0;
min-width: 0;
width: 0;
`;
const StyledCard = styled.div`
background: ${themeCssVariables.background.primary};
border-left: 1px solid ${themeCssVariables.border.color.medium};
border-right: 1px solid ${themeCssVariables.border.color.medium};
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 PageCardLayout = ({
header,
secondaryBar,
children,
showInformationBanner = true,
}: PageCardLayoutProps) => {
return (
<StyledRoot>
<StyledMainCardWrapper>
<StyledCard>
{header}
{secondaryBar}
<StyledBodyContent>
{showInformationBanner && <InformationBannerWrapper />}
{children}
</StyledBodyContent>
</StyledCard>
</StyledMainCardWrapper>
</StyledRoot>
);
};

View File

@@ -5,7 +5,6 @@ import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
const StyledPanel = styled.div`
background: ${themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
flex-direction: column;
height: 100%;

View File

@@ -21,7 +21,6 @@ const StyledInnerContainer = styled.div`
const StyledScrollWrapperContainer = styled.div`
background-color: ${themeCssVariables.background.secondary};
border-radius: ${themeCssVariables.border.radius.md};
overflow: hidden;
`;

View File

@@ -4,12 +4,19 @@ import { useResizablePanel } from '@/ui/layout/resizable-panel/hooks/useResizabl
import { type ResizablePanelConstraints } from '@/ui/layout/resizable-panel/types/ResizablePanelConstraints';
import { type ResizablePanelSide } from '@/ui/layout/resizable-panel/types/ResizablePanelSide';
// Horizontal padding offset by an equal negative margin keeps the resize
// handle grabbable even when the visual gap is 0, without shifting neighbors.
const StyledGap = styled.div<{ gapWidth: number }>`
box-sizing: content-box;
cursor: col-resize;
flex-shrink: 0;
height: 100%;
margin: 0 -4px;
padding: 0 4px;
position: relative;
transition: width 0.15s ease;
width: ${({ gapWidth }) => gapWidth}px;
z-index: 1;
`;
type ResizablePanelGapProps = {

View File

@@ -7,13 +7,12 @@ import { TimelineActivityContext } from '@/activities/timeline-activities/contex
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { PageLayoutRecordPageRenderer } from '@/object-record/record-show/components/PageLayoutRecordPageRenderer';
import { RecordShowPageSSESubscribeEffect } from '@/object-record/record-show/components/RecordShowPageSSESubscribeEffect';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
import { RecordShowPageTitle } from '~/pages/object-record/RecordShowPageTitle';
@@ -46,38 +45,39 @@ export const RecordShowPage = () => {
<CommandMenuComponentInstanceContext.Provider
value={{ instanceId: recordShowComponentInstanceId }}
>
<PageContainer>
<RecordShowPageTitle
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
/>
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
>
<RecordShowCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</RecordShowPageHeader>
<MainContainerLayoutWithSidePanel>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
<RecordShowPageTitle
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
/>
<PageCardLayout
header={
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
>
<PageLayoutRecordPageRenderer
targetRecordIdentifier={{
id: objectRecordId,
targetObjectNameSingular: objectNameSingular,
}}
isInSidePanel={false}
/>
<RecordShowPageSSESubscribeEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
</TimelineActivityContext.Provider>
</MainContainerLayoutWithSidePanel>
</PageContainer>
<RecordShowCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</RecordShowPageHeader>
}
>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<PageLayoutRecordPageRenderer
targetRecordIdentifier={{
id: objectRecordId,
targetObjectNameSingular: objectNameSingular,
}}
isInSidePanel={false}
/>
<RecordShowPageSSESubscribeEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
</TimelineActivityContext.Provider>
</PageCardLayout>
</CommandMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>

View File

@@ -1,7 +1,7 @@
import { getObjectMetadataIdentifierFields } from '@/object-metadata/utils/getObjectMetadataIdentifierFields';
import { ObjectRecordShowPageBreadcrumb } from '@/object-record/record-show/components/ObjectRecordShowPageBreadcrumb';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
export const RecordShowPageHeader = ({
objectNameSingular,
@@ -21,8 +21,8 @@ export const RecordShowPageHeader = ({
getObjectMetadataIdentifierFields({ objectMetadataItem });
return (
<PageHeader
title={
<PageCardHeader
breadcrumb={
<ObjectRecordShowPageBreadcrumb
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
@@ -30,8 +30,7 @@ export const RecordShowPageHeader = ({
labelIdentifierFieldMetadataItem={labelIdentifierFieldMetadataItem}
/>
}
>
{children}
</PageHeader>
actionButton={children}
/>
);
};

View File

@@ -2,10 +2,12 @@ import { StandalonePageCommandMenu } from '@/command-menu-item/components/Standa
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui-deprecated/display';
import { ThemeContext } from 'twenty-ui-deprecated/theme-constants';
type StandalonePageHeaderProps = {
pageLayoutId: string;
@@ -15,6 +17,7 @@ export const StandalonePageHeader = ({
pageLayoutId,
}: StandalonePageHeaderProps) => {
const { getIcon } = useIcons();
const { theme } = useContext(ThemeContext);
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
@@ -30,9 +33,15 @@ export const StandalonePageHeader = ({
: undefined;
return (
<PageHeader title={title} Icon={Icon}>
<StandalonePageCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</PageHeader>
<PageCardHeader
icon={isDefined(Icon) ? <Icon size={theme.icon.size.md} /> : undefined}
title={title}
actionButton={
<>
<StandalonePageCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</>
}
/>
);
};

View File

@@ -1,16 +1,24 @@
import { styled } from '@linaria/react';
import { useParams } from 'react-router-dom';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
import { StandalonePageHeader } from '~/pages/page-layout/StandalonePageHeader';
const StyledPageLayoutContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow-y: auto;
`;
export const StandalonePageLayoutPage = () => {
const { pageLayoutId } = useParams<{ pageLayoutId: string }>();
@@ -19,14 +27,15 @@ export const StandalonePageLayoutPage = () => {
}
return (
<PageContainer>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
>
<CommandMenuComponentInstanceContext.Provider
value={{ instanceId: pageLayoutId }}
>
<CommandMenuComponentInstanceContext.Provider
value={{ instanceId: pageLayoutId }}
<PageCardLayout
header={<StandalonePageHeader pageLayoutId={pageLayoutId} />}
>
<StandalonePageHeader pageLayoutId={pageLayoutId} />
<LayoutRenderingProvider
value={{
targetRecordIdentifier: undefined,
@@ -34,12 +43,12 @@ export const StandalonePageLayoutPage = () => {
isInSidePanel: false,
}}
>
<MainContainerLayoutWithSidePanel>
<StyledPageLayoutContainer>
<PageLayoutRenderer pageLayoutId={pageLayoutId} />
</MainContainerLayoutWithSidePanel>
</StyledPageLayoutContainer>
</LayoutRenderingProvider>
</CommandMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</PageContainer>
</PageCardLayout>
</CommandMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
);
};