Create old fields design widget (#15645)

In this PR:

- Pass `layoutMode` and `tabId` via PageLayoutContentContext provider
- Getting `pageLayoutType` from the current page layout
- Getting isInPinnedTab through `useIsInPinnedTab` hook

## Before

<img width="3456" height="2160" alt="CleanShot 2025-11-06 at 14 22
44@2x"
src="https://github.com/user-attachments/assets/763bb413-5739-45ef-85ed-82a72415886f"
/>

## After

<img width="3456" height="2162" alt="CleanShot 2025-11-06 at 14 20
38@2x"
src="https://github.com/user-attachments/assets/eee6cccd-9d36-426e-a22f-400e8f7f9413"
/>

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Baptiste Devessier
2025-11-06 18:16:42 +01:00
committed by GitHub
parent 33c08ad437
commit bfe1f47065
20 changed files with 328 additions and 107 deletions
@@ -1,8 +1,11 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@@ -17,7 +20,6 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
import { PageLayoutType } from '~/generated/graphql';
export const WidgetPlaceholder = () => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -33,6 +35,10 @@ export const WidgetPlaceholder = () => {
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
const { layoutMode } = usePageLayoutContentContext();
const { isInPinnedTab } = useIsInPinnedTab();
const handleClick = () => {
if (!isPageLayoutInEditMode) {
setIsPageLayoutInEditMode(true);
@@ -44,11 +50,12 @@ export const WidgetPlaceholder = () => {
return (
<WidgetCard
onClick={handleClick}
pageLayoutType={PageLayoutType.DASHBOARD}
layoutMode="grid"
layoutMode={layoutMode}
pageLayoutType={currentPageLayout.type}
isInPinnedTab={isInPinnedTab}
isEditing={false}
isDragging={false}
onClick={handleClick}
>
<WidgetCardHeader
isWidgetCardHovered={false}
@@ -1,11 +1,13 @@
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { type PageLayoutTabLayoutMode } from '@/page-layout/types/PageLayoutTabLayoutMode';
import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay';
import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer';
import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
import { useWidgetPermissions } from '@/page-layout/widgets/hooks/useWidgetPermissions';
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/WidgetCardContent';
@@ -18,15 +20,9 @@ import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
type WidgetRendererProps = {
widget: PageLayoutWidget;
pageLayoutType: PageLayoutType;
layoutMode: PageLayoutTabLayoutMode;
};
export const WidgetRenderer = ({
widget,
pageLayoutType,
layoutMode,
}: WidgetRendererProps) => {
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const theme = useTheme();
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
const { handleEditWidget } = useEditPageLayoutWidget();
@@ -49,6 +45,13 @@ export const WidgetRenderer = ({
const { hasAccess, restriction } = useWidgetPermissions(widget);
const { layoutMode } = usePageLayoutContentContext();
const { isInPinnedTab } = useIsInPinnedTab();
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
const showHeader = layoutMode !== 'canvas' && !isInPinnedTab;
const handleClick = () => {
handleEditWidget({
widgetId: widget.id,
@@ -73,15 +76,16 @@ export const WidgetRenderer = ({
return (
<WidgetCard
onClick={isPageLayoutInEditMode ? handleClick : undefined}
isDragging={isDragging}
pageLayoutType={pageLayoutType}
layoutMode={layoutMode}
isEditing={isEditing}
isDragging={isDragging}
layoutMode={layoutMode}
pageLayoutType={currentPageLayout.type}
isInPinnedTab={isInPinnedTab}
onClick={isPageLayoutInEditMode ? handleClick : undefined}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{layoutMode !== 'canvas' && (
{showHeader && (
<WidgetCardHeader
isWidgetCardHovered={isHovered}
isInEditMode={isPageLayoutInEditMode}
@@ -99,11 +103,13 @@ export const WidgetRenderer = ({
)}
<WidgetCardContent
pageLayoutType={pageLayoutType}
layoutMode={layoutMode}
pageLayoutType={currentPageLayout.type}
isInPinnedTab={isInPinnedTab}
isPageLayoutInEditMode={isPageLayoutInEditMode}
>
{hasAccess && <WidgetContentRenderer widget={widget} />}
{!hasAccess && pageLayoutType === PageLayoutType.DASHBOARD && (
{!hasAccess && currentPageLayout.type === PageLayoutType.DASHBOARD && (
<IconLock
color={theme.font.color.tertiary}
stroke={theme.icon.stroke.sm}
@@ -1,13 +1,31 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { type Meta, type StoryObj } from '@storybook/react';
import { type MutableSnapshot } from 'recoil';
import { ComponentDecorator } from 'twenty-ui/testing';
import { PageLayoutType } from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
const mockPageLayout: PageLayout = {
id: 'page-layout-1',
name: 'Test Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
tabs: [],
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
};
const meta: Meta<typeof WidgetPlaceholder> = {
title: 'Modules/PageLayout/Widgets/WidgetPlaceholder',
component: WidgetPlaceholder,
@@ -19,11 +37,29 @@ const meta: Meta<typeof WidgetPlaceholder> = {
generatedMockObjectMetadataItems,
);
snapshot.set(shouldAppBeLoadingState, false);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
mockPageLayout,
);
};
return (
<PageLayoutTestWrapper initializeState={initializeState}>
<Story />
<LayoutRenderingProvider
value={{
isInRightDrawer: false,
layoutType: PageLayoutType.DASHBOARD,
targetRecordIdentifier: undefined,
}}
>
<PageLayoutContentProvider
value={{ tabId: 'tab-1', layoutMode: 'grid' }}
>
<Story />
</PageLayoutContentProvider>
</LayoutRenderingProvider>
</PageLayoutTestWrapper>
);
},
@@ -12,9 +12,15 @@ import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreCl
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generateGroupByQuery';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import {
GraphOrderBy,
GraphType,
@@ -124,6 +130,21 @@ const meta: Meta<typeof WidgetRenderer> = {
generatedMockObjectMetadataItems,
);
snapshot.set(shouldAppBeLoadingState, false);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
{
id: PAGE_LAYOUT_TEST_INSTANCE_ID,
name: 'Mock Page Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: companyObjectMetadataItem.id,
tabs: [],
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
);
};
return (
@@ -131,7 +152,26 @@ const meta: Meta<typeof WidgetRenderer> = {
<JestMetadataAndApolloMocksWrapper>
<CoreClientProviderWrapper>
<PageLayoutTestWrapper initializeState={initializeState}>
<Story />
<LayoutRenderingProvider
value={{
isInRightDrawer: false,
layoutType: PageLayoutType.DASHBOARD,
targetRecordIdentifier: {
id: companyObjectMetadataItem.id,
targetObjectNameSingular:
companyObjectMetadataItem.nameSingular,
},
}}
>
<PageLayoutContentProvider
value={{
layoutMode: 'grid',
tabId: 'fields',
}}
>
<Story />
</PageLayoutContentProvider>
</LayoutRenderingProvider>
</PageLayoutTestWrapper>
</CoreClientProviderWrapper>
</JestMetadataAndApolloMocksWrapper>
@@ -183,11 +223,7 @@ export const WithNumberChart: Story = {
},
render: (args) => (
<div style={{ width: '300px', height: '100px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -222,11 +258,7 @@ export const WithGaugeChart: Story = {
},
render: (args) => (
<div style={{ width: '300px', height: '400px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -264,11 +296,7 @@ export const WithBarChart: Story = {
},
render: (args) => (
<div style={{ width: '300px', height: '500px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -310,11 +338,7 @@ export const SmallWidget: Story = {
},
render: (args) => (
<div style={{ width: '300px', height: '100px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -359,11 +383,7 @@ export const MediumWidget: Story = {
},
render: (args) => (
<div style={{ width: '400px', height: '250px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -408,11 +428,7 @@ export const LargeWidget: Story = {
},
render: (args) => (
<div style={{ width: '600px', height: '400px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -454,11 +470,7 @@ export const WideWidget: Story = {
},
render: (args) => (
<div style={{ width: '800px', height: '200px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -503,11 +515,7 @@ export const TallWidget: Story = {
},
render: (args) => (
<div style={{ width: '300px', height: '500px' }}>
<WidgetRenderer
widget={args.widget}
layoutMode="grid"
pageLayoutType={PageLayoutType.DASHBOARD}
/>
<WidgetRenderer widget={args.widget} />
</div>
),
};
@@ -0,0 +1,24 @@
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
import { getTabsByDisplayMode } from '@/page-layout/utils/getTabsByDisplayMode';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
export const useIsInPinnedTab = () => {
const isMobile = useIsMobile();
const { tabId } = usePageLayoutContentContext();
const { isInRightDrawer } = useLayoutRenderingContext();
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
const { pinnedLeftTab } = getTabsByDisplayMode({
pageLayout: currentPageLayout,
isMobile,
isInRightDrawer,
});
return {
isInPinnedTab: isDefined(pinnedLeftTab) && pinnedLeftTab.id === tabId,
};
};
@@ -14,6 +14,7 @@ export type WidgetCardProps = {
onClick?: () => void;
isEditing: boolean;
isDragging: boolean;
isInPinnedTab: boolean;
className?: string;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
@@ -23,6 +24,7 @@ const StyledWidgetCard = styled.div<{
onClick?: () => void;
pageLayoutType: PageLayoutType;
layoutMode: PageLayoutTabLayoutMode;
isInPinnedTab: boolean;
isPageLayoutInEditMode: boolean;
isEditing: boolean;
isDragging: boolean;
@@ -41,6 +43,7 @@ const StyledWidgetCard = styled.div<{
isPageLayoutInEditMode,
isEditing,
isDragging,
isInPinnedTab,
onClick,
}) => {
if (layoutMode === 'canvas') {
@@ -50,7 +53,7 @@ const StyledWidgetCard = styled.div<{
}
switch (pageLayoutType) {
case PageLayoutType.DASHBOARD:
case PageLayoutType.DASHBOARD: {
return css`
background: ${theme.background.secondary};
border: 1px solid ${theme.border.color.light};
@@ -85,8 +88,9 @@ const StyledWidgetCard = styled.div<{
border: 1px solid ${theme.color.blue} !important;
`}
`;
}
case PageLayoutType.RECORD_PAGE:
case PageLayoutType.RECORD_PAGE: {
return css`
background: ${theme.background.primary};
border: 1px solid transparent;
@@ -120,10 +124,20 @@ const StyledWidgetCard = styled.div<{
${theme.background.secondary};
border: 1px solid ${theme.color.blue} !important;
`}
${isInPinnedTab &&
!isPageLayoutInEditMode &&
css`
border: none;
padding: 0;
border-radius: 0;
background: ${theme.background.secondary};
`}
`;
}
default:
return '';
return undefined;
}
}}
`;
@@ -135,6 +149,7 @@ export const WidgetCard = ({
onClick,
isEditing,
isDragging,
isInPinnedTab,
className,
onMouseEnter,
onMouseLeave,
@@ -151,6 +166,7 @@ export const WidgetCard = ({
isPageLayoutInEditMode={isPageLayoutInEditMode}
isEditing={isEditing}
isDragging={isDragging}
isInPinnedTab={isInPinnedTab}
className={className}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
@@ -7,6 +7,8 @@ import { type PageLayoutType } from '~/generated/graphql';
export type WidgetCardContentProps = {
children?: ReactNode;
pageLayoutType: PageLayoutType;
isInPinnedTab: boolean;
isPageLayoutInEditMode: boolean;
layoutMode: PageLayoutTabLayoutMode;
className?: string;
};
@@ -20,7 +22,13 @@ const StyledWidgetCardContent = styled.div<WidgetCardContentProps>`
box-sizing: border-box;
padding: ${({ theme }) => theme.spacing(2)};
${({ theme, pageLayoutType, layoutMode }) => {
${({
theme,
pageLayoutType,
layoutMode,
isPageLayoutInEditMode,
isInPinnedTab,
}) => {
if (layoutMode === 'canvas') {
return css`
padding: 0;
@@ -32,6 +40,13 @@ const StyledWidgetCardContent = styled.div<WidgetCardContentProps>`
return css`
border: 1px solid ${theme.border.color.medium};
border-radius: ${theme.border.radius.md};
${isInPinnedTab &&
!isPageLayoutInEditMode &&
css`
border: none;
padding: 0;
`}
`;
default:
@@ -73,6 +73,7 @@ export const Default: Story = {
args: {
pageLayoutType: PageLayoutType.DASHBOARD,
layoutMode: 'grid',
isInPinnedTab: false,
isEditing: false,
isDragging: false,
},
@@ -80,6 +81,7 @@ export const Default: Story = {
<WidgetCard
pageLayoutType={args.pageLayoutType}
layoutMode={args.layoutMode}
isInPinnedTab={args.isInPinnedTab}
isEditing={args.isEditing}
isDragging={args.isDragging}
>
@@ -92,6 +94,8 @@ export const Default: Story = {
<WidgetCardContent
pageLayoutType={args.pageLayoutType}
layoutMode={args.layoutMode}
isInPinnedTab={args.isInPinnedTab}
isPageLayoutInEditMode={args.isEditing}
>
<StyledMockContent>Widget</StyledMockContent>
</WidgetCardContent>
@@ -119,7 +123,9 @@ export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
name: 'contextVariant',
values: [
'Record Page - Default',
'Record Page - Default - Pinned',
'Record Page - Restriction',
'Record Page - Restriction - Pinned',
'Dashboard - Default',
'Dashboard - Restriction',
],
@@ -128,11 +134,13 @@ export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
? PageLayoutType.RECORD_PAGE
: PageLayoutType.DASHBOARD;
const hasRestriction = contextName.includes('Restriction');
const isInPinnedTab = contextName.includes('Pinned');
return {
pageLayoutType,
contextVariant: contextName,
hasRestriction,
isInPinnedTab,
};
},
},
@@ -171,7 +179,9 @@ export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
const isReadMode = args.state === 'Read Mode';
const pageLayoutType = args.pageLayoutType || PageLayoutType.DASHBOARD;
const layoutMode = args.layoutMode || 'grid';
const isInPinnedTab = args.isInPinnedTab || false;
const hasRestriction = args.hasRestriction || false;
const isPageLayoutInEditMode = false;
return (
<PageLayoutTestWrapper>
@@ -182,6 +192,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
isEditing={args.isEditing ?? false}
pageLayoutType={pageLayoutType}
layoutMode={layoutMode}
isInPinnedTab={isInPinnedTab}
>
<WidgetCardHeader
forbiddenDisplay={
@@ -195,6 +206,8 @@ export const Catalog: CatalogStory<Story, typeof WidgetCard> = {
<WidgetCardContent
pageLayoutType={pageLayoutType}
layoutMode={layoutMode}
isInPinnedTab={isInPinnedTab}
isPageLayoutInEditMode={isPageLayoutInEditMode}
>
<StyledMockContent>Widget</StyledMockContent>
</WidgetCardContent>