[Dashboards] - add iframe widget (#14251)
closes https://github.com/twentyhq/core-team-issues/issues/1415
This commit is contained in:
@@ -4,6 +4,7 @@ import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/Com
|
||||
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
|
||||
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
|
||||
import { CommandMenuPageLayoutGraphTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect';
|
||||
import { CommandMenuPageLayoutIframeConfig } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig';
|
||||
import { CommandMenuPageLayoutWidgetTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect';
|
||||
import { CommandMenuMergeRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuMergeRecordPage';
|
||||
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
|
||||
@@ -48,4 +49,8 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
|
||||
CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
<CommandMenuPageLayoutGraphTypeSelect />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutIframeConfig,
|
||||
<CommandMenuPageLayoutIframeConfig />,
|
||||
],
|
||||
]);
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { useCreatePageLayoutIframeWidget } from '@/settings/page-layout/hooks/useCreatePageLayoutIframeWidget';
|
||||
import { usePageLayoutWidgetUpdate } from '@/settings/page-layout/hooks/usePageLayoutWidgetUpdate';
|
||||
import { pageLayoutEditingWidgetIdState } from '@/settings/page-layout/states/pageLayoutEditingWidgetIdState';
|
||||
import { pageLayoutWidgetsState } from '@/settings/page-layout/states/pageLayoutWidgetsState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { isValidUrl } from 'twenty-shared/utils';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const CommandMenuPageLayoutIframeConfig = () => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { createPageLayoutIframeWidget } = useCreatePageLayoutIframeWidget();
|
||||
const { handleUpdateWidget } = usePageLayoutWidgetUpdate();
|
||||
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
|
||||
useRecoilState(pageLayoutEditingWidgetIdState);
|
||||
const pageLayoutWidgets = useRecoilValue(pageLayoutWidgetsState);
|
||||
|
||||
const editingWidget = pageLayoutWidgets.find(
|
||||
(w) => w.id === pageLayoutEditingWidgetId,
|
||||
);
|
||||
const isEditMode = !!editingWidget;
|
||||
|
||||
const [title, setTitle] = useState(editingWidget?.title || '');
|
||||
const [url, setUrl] = useState(editingWidget?.configuration?.url || '');
|
||||
const [urlError, setUrlError] = useState('');
|
||||
|
||||
const validateUrl = (urlString: string): boolean => {
|
||||
const trimmedUrl = urlString.trim();
|
||||
|
||||
if (!isValidUrl(trimmedUrl)) {
|
||||
setUrlError('Please enter a valid URL');
|
||||
return false;
|
||||
}
|
||||
|
||||
setUrlError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleUrlChange = (value: string) => {
|
||||
setUrl(value);
|
||||
validateUrl(value);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!title.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateUrl(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditMode && pageLayoutEditingWidgetId !== null) {
|
||||
handleUpdateWidget(pageLayoutEditingWidgetId, {
|
||||
title: title.trim(),
|
||||
configuration: {
|
||||
...editingWidget?.configuration,
|
||||
url: url.trim(),
|
||||
},
|
||||
});
|
||||
setPageLayoutEditingWidgetId(null);
|
||||
} else {
|
||||
createPageLayoutIframeWidget(title.trim(), url.trim());
|
||||
}
|
||||
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
const isFormValid = title.trim() && url.trim() && !urlError;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>
|
||||
{isEditMode ? 'Edit iFrame Widget' : 'Configure iFrame Widget'}
|
||||
</StyledSectionTitle>
|
||||
|
||||
<FormTextFieldInput
|
||||
label="Widget Title"
|
||||
placeholder="e.g., Analytics Dashboard"
|
||||
defaultValue={title}
|
||||
onChange={setTitle}
|
||||
/>
|
||||
|
||||
<FormTextFieldInput
|
||||
label="URL to Embed"
|
||||
placeholder="https://example.com/embed"
|
||||
defaultValue={url}
|
||||
onChange={handleUrlChange}
|
||||
error={urlError}
|
||||
/>
|
||||
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
title={isEditMode ? 'Save Changes' : 'Create Widget'}
|
||||
onClick={handleSubmit}
|
||||
disabled={!isFormValid}
|
||||
variant="primary"
|
||||
size="small"
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+19
-9
@@ -45,7 +45,7 @@ const widgetTypeOptions = [
|
||||
type: WidgetType.IFRAME,
|
||||
icon: IconFrame,
|
||||
title: 'Add an iframe',
|
||||
disabled: true,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -56,14 +56,24 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
|
||||
);
|
||||
|
||||
const handleSelectWidget = (widgetType: WidgetType) => {
|
||||
if (widgetType === WidgetType.GRAPH) {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
pageTitle: 'Select Graph Type',
|
||||
pageIcon: IconChartPie,
|
||||
});
|
||||
} else {
|
||||
setPageLayoutDraggedArea(null);
|
||||
switch (widgetType) {
|
||||
case WidgetType.GRAPH:
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
pageTitle: 'Select Graph Type',
|
||||
pageIcon: IconChartPie,
|
||||
});
|
||||
break;
|
||||
case WidgetType.IFRAME:
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.PageLayoutIframeConfig,
|
||||
pageTitle: 'Configure iFrame',
|
||||
pageIcon: IconFrame,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
setPageLayoutDraggedArea(null);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,4 +16,5 @@ export enum CommandMenuPages {
|
||||
ViewPreviousAIChats = 'view-previous-ai-chats',
|
||||
PageLayoutWidgetTypeSelect = 'page-layout-widget-type-select',
|
||||
PageLayoutGraphTypeSelect = 'page-layout-graph-type-select',
|
||||
PageLayoutIframeConfig = 'page-layout-iframe-config',
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
border: none;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLoadingContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledErrorContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledErrorUrl = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
word-break: break-all;
|
||||
`;
|
||||
|
||||
export type IframeWidgetProps = {
|
||||
url: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const IframeWidget = ({
|
||||
url,
|
||||
title = 'Embedded Content',
|
||||
}: IframeWidgetProps) => {
|
||||
const theme = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleIframeError = () => {
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
};
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledErrorContainer>
|
||||
<StyledErrorMessage>Failed to load content</StyledErrorMessage>
|
||||
<StyledErrorUrl>{url}</StyledErrorUrl>
|
||||
</StyledErrorContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{isLoading && (
|
||||
<StyledLoadingContainer>
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Skeleton
|
||||
width={120}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
|
||||
/>
|
||||
</SkeletonTheme>
|
||||
</StyledLoadingContainer>
|
||||
)}
|
||||
<StyledIframe
|
||||
src={url}
|
||||
title={title}
|
||||
onLoad={handleIframeLoad}
|
||||
onError={handleIframeError}
|
||||
sandbox="allow-scripts allow-forms allow-popups"
|
||||
allow="encrypted-media"
|
||||
allowFullScreen
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,8 +14,10 @@ import { usePageLayoutDragSelection } from '@/settings/page-layout/hooks/usePage
|
||||
import { usePageLayoutHandleLayoutChange } from '@/settings/page-layout/hooks/usePageLayoutHandleLayoutChange';
|
||||
import { usePageLayoutSaveHandler } from '@/settings/page-layout/hooks/usePageLayoutSaveHandler';
|
||||
import { usePageLayoutWidgetDelete } from '@/settings/page-layout/hooks/usePageLayoutWidgetDelete';
|
||||
import { WidgetType } from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { pageLayoutCurrentBreakpointState } from '@/settings/page-layout/states/pageLayoutCurrentBreakpointState';
|
||||
import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutEditingWidgetIdState } from '@/settings/page-layout/states/pageLayoutEditingWidgetIdState';
|
||||
import { pageLayoutSelectedCellsState } from '@/settings/page-layout/states/pageLayoutSelectedCellsState';
|
||||
import { pageLayoutWidgetsState } from '@/settings/page-layout/states/pageLayoutWidgetsState';
|
||||
import { calculateTotalGridRows } from '@/settings/page-layout/utils/calculateTotalGridRows';
|
||||
@@ -36,7 +38,7 @@ import {
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconAppWindow, IconPlus } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
@@ -134,6 +136,9 @@ export const SettingsPageLayoutEdit = () => {
|
||||
);
|
||||
const pageLayoutWidgets = useRecoilValue(pageLayoutWidgetsState);
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const setPageLayoutEditingWidgetId = useSetRecoilState(
|
||||
pageLayoutEditingWidgetIdState,
|
||||
);
|
||||
|
||||
const {
|
||||
handleDragSelectionStart,
|
||||
@@ -153,6 +158,25 @@ export const SettingsPageLayoutEdit = () => {
|
||||
const { handleRemoveWidget } = usePageLayoutWidgetDelete();
|
||||
const { handleLayoutChange } = usePageLayoutHandleLayoutChange();
|
||||
|
||||
const handleEditWidget = useCallback(
|
||||
(widgetId: string) => {
|
||||
const widget = pageLayoutWidgets.find((w) => w.id === widgetId);
|
||||
if (!widget) return;
|
||||
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
|
||||
if (widget.type === WidgetType.IFRAME) {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.PageLayoutIframeConfig,
|
||||
pageTitle: 'Edit iFrame',
|
||||
pageIcon: IconAppWindow,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[pageLayoutWidgets, setPageLayoutEditingWidgetId, navigateCommandMenu],
|
||||
);
|
||||
|
||||
const gridContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isEmptyState = pageLayoutWidgets.length === 0;
|
||||
@@ -289,6 +313,7 @@ export const SettingsPageLayoutEdit = () => {
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={widget.title}
|
||||
onRemove={() => handleRemoveWidget(widget.id)}
|
||||
onEdit={() => handleEditWidget(widget.id)}
|
||||
>
|
||||
{renderWidget(widget)}
|
||||
</PageLayoutWidgetPlaceholder>
|
||||
|
||||
Reference in New Issue
Block a user