[Dashboards] - add iframe widget (#14251)

closes https://github.com/twentyhq/core-team-issues/issues/1415
This commit is contained in:
nitin
2025-09-03 21:01:38 +05:30
committed by GitHub
parent 7ac7f52510
commit 0a6d5479c9
37 changed files with 522 additions and 78 deletions
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { IconGripVertical, IconX } from 'twenty-ui/display';
import { IconGripVertical, IconPencil, IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import {
AnimatedPlaceholder,
@@ -66,6 +66,7 @@ const StyledContent = styled.div`
type PageLayoutWidgetPlaceholderProps = {
title?: string;
onRemove?: () => void;
onEdit?: () => void;
children?: ReactNode;
isEmpty?: boolean;
};
@@ -73,6 +74,7 @@ type PageLayoutWidgetPlaceholderProps = {
export const PageLayoutWidgetPlaceholder = ({
title = 'Graph Title',
onRemove,
onEdit,
children,
isEmpty = false,
}: PageLayoutWidgetPlaceholderProps) => {
@@ -116,6 +118,14 @@ export const PageLayoutWidgetPlaceholder = ({
size="small"
/>
<StyledTitle>{title}</StyledTitle>
{onEdit && (
<IconButton
onClick={onEdit}
Icon={IconPencil}
variant="tertiary"
size="small"
/>
)}
{onRemove && (
<IconButton
onClick={onRemove}
@@ -1,6 +1,6 @@
import { GraphWidgetGaugeChart } from '@/dashboards/graphs/components/GraphWidgetGaugeChart';
import { GraphWidgetNumberChart } from '@/dashboards/graphs/components/GraphWidgetNumberChart';
import { GraphWidgetPieChart } from '@/dashboards/graphs/components/GraphWidgetPieChart';
import { GraphWidgetGaugeChart } from '@/dashboards/widgets/graph/components/GraphWidgetGaugeChart';
import { GraphWidgetNumberChart } from '@/dashboards/widgets/graph/components/GraphWidgetNumberChart';
import { GraphWidgetPieChart } from '@/dashboards/widgets/graph/components/GraphWidgetPieChart';
import { type Meta, type StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui/testing';
import { PageLayoutWidgetPlaceholder } from '../PageLayoutWidgetPlaceholder';
@@ -0,0 +1,80 @@
import { useRecoilCallback } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { type Widget, WidgetType } from '../mocks/mockWidgets';
import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState';
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState';
import { pageLayoutWidgetsState } from '../states/pageLayoutWidgetsState';
import { getDefaultWidgetPosition } from '../utils/getDefaultWidgetPosition';
export const useCreatePageLayoutIframeWidget = () => {
const createPageLayoutIframeWidget = useRecoilCallback(
({ snapshot, set }) =>
(title: string, url: string) => {
const pageLayoutWidgets = snapshot
.getLoadable(pageLayoutWidgetsState)
.getValue();
const pageLayoutCurrentLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const pageLayoutDraggedArea = snapshot
.getLoadable(pageLayoutDraggedAreaState)
.getValue();
const newWidget: Widget = {
id: `widget-${uuidv4()}`,
type: WidgetType.IFRAME,
title,
configuration: {
url,
},
};
const defaultSize = { w: 6, h: 6 };
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
);
const newLayout = {
i: newWidget.id,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
};
const updatedWidgets = [...pageLayoutWidgets, newWidget];
set(pageLayoutWidgetsState, updatedWidgets);
const updatedLayouts = {
desktop: [...(pageLayoutCurrentLayouts.desktop || []), newLayout],
mobile: [
...(pageLayoutCurrentLayouts.mobile || []),
{ ...newLayout, w: 1, x: 0 },
],
};
set(pageLayoutCurrentLayoutsState, updatedLayouts);
const widgetWithPosition = {
...newWidget,
gridPosition: {
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
};
set(pageLayoutDraftState, (prev) => ({
...prev,
widgets: [...prev.widgets, widgetWithPosition],
}));
set(pageLayoutDraggedAreaState, null);
},
[],
);
return { createPageLayoutIframeWidget };
};
@@ -0,0 +1,35 @@
import { useRecoilCallback } from 'recoil';
import { type Widget } from '../mocks/mockWidgets';
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
import { pageLayoutWidgetsState } from '../states/pageLayoutWidgetsState';
export const usePageLayoutWidgetUpdate = () => {
const handleUpdateWidget = useRecoilCallback(
({ snapshot, set }) =>
(widgetId: string, updates: Partial<Widget>) => {
const pageLayoutWidgets = snapshot
.getLoadable(pageLayoutWidgetsState)
.getValue();
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
const updatedWidgets = pageLayoutWidgets.map((widget) =>
widget.id === widgetId ? { ...widget, ...updates } : widget,
);
set(pageLayoutWidgetsState, updatedWidgets);
const updatedDraftWidgets = pageLayoutDraft.widgets.map((widget) =>
widget.id === widgetId ? { ...widget, ...updates } : widget,
);
set(pageLayoutDraftState, {
...pageLayoutDraft,
widgets: updatedDraftWidgets,
});
},
[],
);
return { handleUpdateWidget };
};
@@ -18,7 +18,7 @@ export type Widget = {
id: string;
type: WidgetType;
title: string;
configuration?: Record<string, unknown>;
configuration?: Record<string, string>;
data?: any;
};
@@ -0,0 +1,6 @@
import { createState } from 'twenty-ui/utilities';
export const pageLayoutEditingWidgetIdState = createState<string | null>({
key: 'pageLayoutEditingWidgetIdState',
defaultValue: null,
});
@@ -23,7 +23,7 @@ export type SavedPageLayout = {
rowSpan: number;
columnSpan: number;
};
configuration?: Record<string, unknown>;
configuration?: Record<string, string>;
data?: any; // TODO: Remove when backend connected - data will be fetched dynamically
}>;
};
@@ -0,0 +1,66 @@
import { GraphWidgetBarChart } from '@/dashboards/widgets/graph/components/GraphWidgetBarChart';
import { GraphWidgetGaugeChart } from '@/dashboards/widgets/graph/components/GraphWidgetGaugeChart';
import { GraphWidgetNumberChart } from '@/dashboards/widgets/graph/components/GraphWidgetNumberChart';
import { GraphWidgetPieChart } from '@/dashboards/widgets/graph/components/GraphWidgetPieChart';
import { type ReactNode } from 'react';
import { GraphSubType, type Widget } from '../mocks/mockWidgets';
type GraphRenderer = (widget: Widget) => ReactNode;
const graphRenderers: Record<GraphSubType, GraphRenderer> = {
[GraphSubType.NUMBER]: (widget) => (
<GraphWidgetNumberChart
value={widget.data.value}
trendPercentage={widget.data.trendPercentage}
/>
),
[GraphSubType.GAUGE]: (widget) => (
<GraphWidgetGaugeChart
data={{
value: widget.data.value,
min: widget.data.min,
max: widget.data.max,
label: widget.data.label,
}}
displayType="percentage"
showValue
id={`gauge-chart-${widget.id}`}
/>
),
[GraphSubType.PIE]: (widget) => (
<GraphWidgetPieChart
data={widget.data.items}
showLegend
displayType="percentage"
id={`pie-chart-${widget.id}`}
/>
),
[GraphSubType.BAR]: (widget) => (
<GraphWidgetBarChart
data={widget.data.items}
indexBy={widget.data.indexBy}
keys={widget.data.keys}
seriesLabels={widget.data.seriesLabels}
layout={widget.data.layout}
showLegend
showGrid
displayType="number"
id={`bar-chart-${widget.id}`}
/>
),
};
export const renderGraphWidget = (widget: Widget): ReactNode => {
const graphType = widget.configuration?.graphType as GraphSubType | undefined;
if (!graphType) {
return null;
}
const renderer = graphRenderers[graphType];
if (!renderer) {
return null;
}
return renderer(widget);
};
@@ -1,69 +1,28 @@
import { GraphWidgetBarChart } from '@/dashboards/graphs/components/GraphWidgetBarChart';
import { GraphWidgetGaugeChart } from '@/dashboards/graphs/components/GraphWidgetGaugeChart';
import { GraphWidgetNumberChart } from '@/dashboards/graphs/components/GraphWidgetNumberChart';
import { GraphWidgetPieChart } from '@/dashboards/graphs/components/GraphWidgetPieChart';
import { IframeWidget } from '@/dashboards/widgets/iframe/components/IframeWidget';
import { type ReactNode } from 'react';
import { GraphSubType, WidgetType, type Widget } from '../mocks/mockWidgets';
type WidgetRenderer = (widget: Widget) => ReactNode;
const widgetRenderers: Record<GraphSubType, WidgetRenderer> = {
[GraphSubType.NUMBER]: (widget) => (
<GraphWidgetNumberChart
value={widget.data.value}
trendPercentage={widget.data.trendPercentage}
/>
),
[GraphSubType.GAUGE]: (widget) => (
<GraphWidgetGaugeChart
data={{
value: widget.data.value,
min: widget.data.min,
max: widget.data.max,
label: widget.data.label,
}}
displayType="percentage"
showValue
id={`gauge-chart-${widget.id}`}
/>
),
[GraphSubType.PIE]: (widget) => (
<GraphWidgetPieChart
data={widget.data.items}
showLegend
displayType="percentage"
id={`pie-chart-${widget.id}`}
/>
),
[GraphSubType.BAR]: (widget) => (
<GraphWidgetBarChart
data={widget.data.items}
indexBy={widget.data.indexBy}
keys={widget.data.keys}
seriesLabels={widget.data.seriesLabels}
layout={widget.data.layout}
showLegend
showGrid
displayType="number"
id={`bar-chart-${widget.id}`}
/>
),
};
import { WidgetType, type Widget } from '../mocks/mockWidgets';
import { renderGraphWidget } from './graphRegistry';
export const renderWidget = (widget: Widget): ReactNode => {
if (widget.type !== WidgetType.GRAPH) {
return null;
}
switch (widget.type) {
case WidgetType.GRAPH:
return renderGraphWidget(widget);
const graphType = widget.configuration?.graphType as GraphSubType | undefined;
if (!graphType) {
return null;
}
case WidgetType.IFRAME:
return (
<IframeWidget
url={widget.configuration?.url ?? ''}
title={widget.title}
/>
);
const renderer = widgetRenderers[graphType];
if (!renderer) {
return null;
}
case WidgetType.VIEW:
return null;
return renderer(widget);
case WidgetType.FIELDS:
return null;
default:
return null;
}
};