diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
index 54cbfae8b44..7c318080d6a 100644
--- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
@@ -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,
,
],
+ [
+ CommandMenuPages.PageLayoutIframeConfig,
+ ,
+ ],
]);
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx
new file mode 100644
index 00000000000..b54c5e0c89d
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx
@@ -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 (
+
+
+ {isEditMode ? 'Edit iFrame Widget' : 'Configure iFrame Widget'}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx
index da7a3c12bed..af8dcb7d8e4 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx
@@ -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;
}
};
diff --git a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
index 6643b6543d0..b6e55830b19 100644
--- a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
+++ b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
@@ -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',
}
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetBarChart.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetBarChart.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetBarChart.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetBarChart.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetGaugeChart.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetGaugeChart.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetGaugeChart.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetGaugeChart.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetLegend.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetLegend.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetLegend.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetLegend.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetNumberChart.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetNumberChart.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetNumberChart.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetNumberChart.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetPieChart.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetPieChart.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetPieChart.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetTooltip.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetTooltip.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/GraphWidgetTooltip.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/GraphWidgetTooltip.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetBarChart.stories.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetBarChart.stories.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetGaugeChart.stories.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetGaugeChart.stories.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetGaugeChart.stories.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetGaugeChart.stories.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetLegend.stories.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetLegend.stories.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetLegend.stories.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetLegend.stories.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetNumberChart.stories.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetNumberChart.stories.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetNumberChart.stories.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetNumberChart.stories.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetPieChart.stories.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetPieChart.stories.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetTooltip.stories.tsx b/packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetTooltip.stories.tsx
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/components/__stories__/GraphWidgetTooltip.stories.tsx
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/components/__stories__/GraphWidgetTooltip.stories.tsx
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColor.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/types/GraphColor.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/types/GraphColor.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/types/GraphColor.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorRegistry.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/types/GraphColorRegistry.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorRegistry.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/types/GraphColorRegistry.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorScheme.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/types/GraphColorScheme.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/types/GraphColorScheme.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/types/GraphColorScheme.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/calculateAngularGradient.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/utils/calculateAngularGradient.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/utils/calculateAngularGradient.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/utils/calculateAngularGradient.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/createGradientDef.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/utils/createGradientDef.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/utils/createGradientDef.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/utils/createGradientDef.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/createGraphColorRegistry.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/utils/createGraphColorRegistry.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/utils/createGraphColorRegistry.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/utils/createGraphColorRegistry.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorScheme.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/utils/getColorScheme.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/utils/getColorScheme.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/utils/getColorScheme.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByIndex.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/utils/getColorSchemeByIndex.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByIndex.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/utils/getColorSchemeByIndex.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByName.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/utils/getColorSchemeByName.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/utils/getColorSchemeByName.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/utils/getColorSchemeByName.ts
diff --git a/packages/twenty-front/src/modules/dashboards/graphs/utils/graphFormatters.ts b/packages/twenty-front/src/modules/dashboards/widgets/graph/utils/graphFormatters.ts
similarity index 100%
rename from packages/twenty-front/src/modules/dashboards/graphs/utils/graphFormatters.ts
rename to packages/twenty-front/src/modules/dashboards/widgets/graph/utils/graphFormatters.ts
diff --git a/packages/twenty-front/src/modules/dashboards/widgets/iframe/components/IframeWidget.tsx b/packages/twenty-front/src/modules/dashboards/widgets/iframe/components/IframeWidget.tsx
new file mode 100644
index 00000000000..6eb30b458c8
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/widgets/iframe/components/IframeWidget.tsx
@@ -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 (
+
+
+ Failed to load content
+ {url}
+
+
+ );
+ }
+
+ return (
+
+ {isLoading && (
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx b/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx
index 3fd145d1240..ed037915340 100644
--- a/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx
+++ b/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutWidgetPlaceholder.tsx
@@ -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"
/>
{title}
+ {onEdit && (
+
+ )}
{onRemove && (
{
+ 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 };
+};
diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetUpdate.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetUpdate.ts
new file mode 100644
index 00000000000..dfdcf1d9bc9
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetUpdate.ts
@@ -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) => {
+ 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 };
+};
diff --git a/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts b/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts
index 9e61103d8d1..80592930d22 100644
--- a/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts
+++ b/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts
@@ -18,7 +18,7 @@ export type Widget = {
id: string;
type: WidgetType;
title: string;
- configuration?: Record;
+ configuration?: Record;
data?: any;
};
diff --git a/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutEditingWidgetIdState.ts b/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutEditingWidgetIdState.ts
new file mode 100644
index 00000000000..8c355275621
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutEditingWidgetIdState.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui/utilities';
+
+export const pageLayoutEditingWidgetIdState = createState({
+ key: 'pageLayoutEditingWidgetIdState',
+ defaultValue: null,
+});
diff --git a/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts b/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts
index 2656d813aeb..154f4e1b539 100644
--- a/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts
+++ b/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts
@@ -23,7 +23,7 @@ export type SavedPageLayout = {
rowSpan: number;
columnSpan: number;
};
- configuration?: Record;
+ configuration?: Record;
data?: any; // TODO: Remove when backend connected - data will be fetched dynamically
}>;
};
diff --git a/packages/twenty-front/src/modules/settings/page-layout/utils/graphRegistry.tsx b/packages/twenty-front/src/modules/settings/page-layout/utils/graphRegistry.tsx
new file mode 100644
index 00000000000..1ca32207638
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/page-layout/utils/graphRegistry.tsx
@@ -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.NUMBER]: (widget) => (
+
+ ),
+ [GraphSubType.GAUGE]: (widget) => (
+
+ ),
+ [GraphSubType.PIE]: (widget) => (
+
+ ),
+ [GraphSubType.BAR]: (widget) => (
+
+ ),
+};
+
+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);
+};
diff --git a/packages/twenty-front/src/modules/settings/page-layout/utils/widgetRegistry.tsx b/packages/twenty-front/src/modules/settings/page-layout/utils/widgetRegistry.tsx
index f6c9fc293dd..ec2ffdfc579 100644
--- a/packages/twenty-front/src/modules/settings/page-layout/utils/widgetRegistry.tsx
+++ b/packages/twenty-front/src/modules/settings/page-layout/utils/widgetRegistry.tsx
@@ -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.NUMBER]: (widget) => (
-
- ),
- [GraphSubType.GAUGE]: (widget) => (
-
- ),
- [GraphSubType.PIE]: (widget) => (
-
- ),
- [GraphSubType.BAR]: (widget) => (
-
- ),
-};
+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 (
+
+ );
- const renderer = widgetRenderers[graphType];
- if (!renderer) {
- return null;
- }
+ case WidgetType.VIEW:
+ return null;
- return renderer(widget);
+ case WidgetType.FIELDS:
+ return null;
+
+ default:
+ return null;
+ }
};
diff --git a/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx b/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx
index 8f09ae5c5fb..60e6d70f5c2 100644
--- a/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx
+++ b/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx
@@ -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(null);
const isEmptyState = pageLayoutWidgets.length === 0;
@@ -289,6 +313,7 @@ export const SettingsPageLayoutEdit = () => {
handleRemoveWidget(widget.id)}
+ onEdit={() => handleEditWidget(widget.id)}
>
{renderWidget(widget)}