Add configuration types for widgets (#14717)

closes https://github.com/twentyhq/core-team-issues/issues/1532
This commit is contained in:
nitin
2025-09-29 17:11:26 +05:30
committed by GitHub
parent 2685f4a5b9
commit ea144c434b
88 changed files with 2829 additions and 1487 deletions
@@ -1,8 +1,13 @@
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div<{ onClick?: () => void }>`
const StyledContainer = styled.div<{
onClick?: () => void;
showHover?: boolean;
}>`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
@@ -14,21 +19,36 @@ const StyledContainer = styled.div<{ onClick?: () => void }>`
position: relative;
padding: ${({ theme }) => theme.spacing(4)};
&:hover {
cursor: ${({ onClick }) => (isDefined(onClick) ? 'pointer' : 'default')};
border: 1px solid ${({ theme }) => theme.color.blue};
background: ${({ theme }) => theme.background.primary};
}
${({ showHover, onClick, theme }) =>
showHover &&
`
&:hover {
cursor: ${isDefined(onClick) ? 'pointer' : 'default'};
border: 1px solid ${theme.color.blue};
background: ${theme.background.primary};
}
`}
`;
type WidgetContainerProps = {
children?: ReactNode;
onClick?: () => void;
isRestricted?: boolean;
};
export const WidgetContainer = ({
children,
onClick,
isRestricted = false,
}: WidgetContainerProps) => {
return <StyledContainer onClick={onClick}>{children}</StyledContainer>;
const isPageLayoutInEditModeComponent = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
const showHover = !isRestricted || isPageLayoutInEditModeComponent;
return (
<StyledContainer onClick={onClick} showHover={showHover}>
{children}
</StyledContainer>
);
};
@@ -1,11 +1,9 @@
import { GraphWidgetRenderer } from '@/page-layout/widgets/graph/components/GraphWidgetRenderer';
import { IframeWidget } from '@/page-layout/widgets/iframe/components/IframeWidget';
import { type Widget } from '@/page-layout/widgets/types/Widget';
import { isString } from '@sniptt/guards';
import { WidgetType } from '~/generated/graphql';
import { type PageLayoutWidget, WidgetType } from '~/generated/graphql';
type WidgetContentRendererProps = {
widget: Widget;
widget: PageLayoutWidget;
};
export const WidgetContentRenderer = ({
@@ -15,12 +13,9 @@ export const WidgetContentRenderer = ({
case WidgetType.GRAPH:
return <GraphWidgetRenderer widget={widget} />;
case WidgetType.IFRAME: {
const url = widget.configuration?.url;
return (
<IframeWidget url={isString(url) ? url : ''} title={widget.title} />
);
}
case WidgetType.IFRAME:
return <IframeWidget widget={widget} />;
default:
return null;
}
@@ -1,15 +1,17 @@
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { WidgetContainer } from '@/page-layout/widgets/components/WidgetContainer';
import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer';
import { WidgetHeader } from '@/page-layout/widgets/components/WidgetHeader';
import { type Widget as WidgetType } from '@/page-layout/widgets/types/Widget';
import { useWidgetObjectPermissions } from '@/page-layout/widgets/hooks/useWidgetObjectPermissions';
import { type PageLayoutWidget } from '~/generated/graphql';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
type WidgetRendererProps = {
widget: WidgetType;
widget: PageLayoutWidget;
};
const StyledContent = styled.div`
@@ -23,13 +25,14 @@ const StyledContent = styled.div`
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
const { handleEditWidget } = useEditPageLayoutWidget();
const { haveAccessToWidgetsObject } = useWidgetObjectPermissions(widget);
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
return (
<WidgetContainer>
<WidgetContainer isRestricted={!haveAccessToWidgetsObject}>
<WidgetHeader
isInEditMode={isPageLayoutInEditMode}
title={widget.title}
@@ -39,7 +42,11 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
onRemove={() => deletePageLayoutWidget(widget.id)}
/>
<StyledContent>
<WidgetContentRenderer widget={widget} />
{!haveAccessToWidgetsObject ? (
<ForbiddenFieldDisplay />
) : (
<WidgetContentRenderer widget={widget} />
)}
</StyledContent>
</WidgetContainer>
);
@@ -1,3 +1,4 @@
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
import { type Meta, type StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui/testing';
@@ -5,7 +6,14 @@ import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta<typeof WidgetPlaceholder> = {
title: 'Modules/PageLayout/Widgets/WidgetPlaceholder',
component: WidgetPlaceholder,
decorators: [ComponentDecorator],
decorators: [
(Story) => (
<PageLayoutTestWrapper>
<Story />
</PageLayoutTestWrapper>
),
ComponentDecorator,
],
parameters: {
layout: 'centered',
docs: {
@@ -1,9 +1,9 @@
import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { GraphType } from '@/page-layout/mocks/mockWidgets';
import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { GraphType } from '~/generated-metadata/graphql';
import { type Meta, type StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui/testing';
import { WidgetType } from '~/generated/graphql';
const meta: Meta<typeof WidgetRenderer> = {
title: 'Modules/PageLayout/Widgets/WidgetRenderer',
@@ -30,23 +30,18 @@ type Story = StoryObj<typeof WidgetRenderer>;
export const WithNumberChart: Story = {
args: {
widget: {
title: 'Sales Pipeline',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
},
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Sales Pipeline',
GraphType.NUMBER,
{
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
render: (args) => (
<div style={{ width: '300px', height: '100px' }}>
@@ -57,23 +52,18 @@ export const WithNumberChart: Story = {
export const WithGaugeChart: Story = {
args: {
widget: {
title: 'Conversion Rate',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.GAUGE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Conversion Rate',
GraphType.GAUGE,
{
row: 0,
column: 0,
rowSpan: 5,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
render: (args) => (
<div style={{ width: '300px', height: '400px' }}>
@@ -84,23 +74,18 @@ export const WithGaugeChart: Story = {
export const WithPieChart: Story = {
args: {
widget: {
title: 'Lead Distribution',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.PIE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Lead Distribution',
GraphType.PIE,
{
row: 0,
column: 0,
rowSpan: 5,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
render: (args) => (
<div style={{ width: '300px', height: '500px' }}>
@@ -111,23 +96,18 @@ export const WithPieChart: Story = {
export const SmallWidget: Story = {
args: {
widget: {
title: 'Small Widget (2x2 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Small Widget (2x2 grid)',
GraphType.NUMBER,
{
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 2,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -145,23 +125,18 @@ export const SmallWidget: Story = {
export const MediumWidget: Story = {
args: {
widget: {
title: 'Medium Widget (4x3 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.GAUGE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Medium Widget (4x3 grid)',
GraphType.GAUGE,
{
row: 0,
column: 0,
rowSpan: 3,
columnSpan: 4,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -179,23 +154,18 @@ export const MediumWidget: Story = {
export const LargeWidget: Story = {
args: {
widget: {
title: 'Large Widget (6x4 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.PIE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Large Widget (6x4 grid)',
GraphType.PIE,
{
row: 0,
column: 0,
rowSpan: 4,
columnSpan: 6,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -213,23 +183,18 @@ export const LargeWidget: Story = {
export const WideWidget: Story = {
args: {
widget: {
title: 'Wide Widget (8x2 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.NUMBER,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Wide Widget (8x2 grid)',
GraphType.NUMBER,
{
row: 0,
column: 0,
rowSpan: 2,
columnSpan: 8,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {
@@ -247,23 +212,18 @@ export const WideWidget: Story = {
export const TallWidget: Story = {
args: {
widget: {
title: 'Tall Widget (3x6 grid)',
type: WidgetType.GRAPH,
configuration: {
graphType: GraphType.GAUGE,
},
createdAt: '2024-01-01T00:00:00Z',
gridPosition: {
widget: createDefaultGraphWidget(
'widget-1',
'tab-overview',
'Tall Widget (3x6 grid)',
GraphType.GAUGE,
{
row: 0,
column: 0,
rowSpan: 6,
columnSpan: 3,
},
id: 'widget-1',
pageLayoutTabId: 'tab-overview',
updatedAt: '2024-01-01T00:00:00Z',
},
),
},
parameters: {
docs: {