[Dashboards] - add iframe widget (#14251)
closes https://github.com/twentyhq/core-team-issues/issues/1415
This commit is contained in:
+11
-1
@@ -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}
|
||||
|
||||
+3
-3
@@ -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';
|
||||
|
||||
+80
@@ -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 };
|
||||
};
|
||||
+35
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const pageLayoutEditingWidgetIdState = createState<string | null>({
|
||||
key: 'pageLayoutEditingWidgetIdState',
|
||||
defaultValue: null,
|
||||
});
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user