Add configuration types for widgets (#14717)
closes https://github.com/twentyhq/core-team-issues/issues/1532
This commit is contained in:
+27
-7
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-10
@@ -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;
|
||||
}
|
||||
|
||||
+11
-4
@@ -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>
|
||||
);
|
||||
|
||||
+9
-1
@@ -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: {
|
||||
|
||||
+58
-98
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user