IFrame widget improvements (#15483)

changes - 
- make iframe side panel to match others -- ie use SidePanelHeader for
title
- make url optional in configuration to match that of the other widgets
(allow partial saves) - render No data status when error or no url
- split widget sizes into two -- graph widget sizes and widget sizes
(graph widgets are a subset of chart widgets)
This commit is contained in:
nitin
2025-11-03 23:00:06 +05:30
committed by GitHub
parent 902eb2c5d2
commit 5bc876e4b4
23 changed files with 222 additions and 258 deletions
@@ -5,7 +5,7 @@ import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-even
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuPageLayoutGraphFilter } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphFilter';
import { CommandMenuPageLayoutGraphTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect';
import { CommandMenuPageLayoutIframeConfig } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig';
import { CommandMenuPageLayoutIframeSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings';
import { CommandMenuPageLayoutWidgetTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect';
import { CommandMenuPageLayoutTabSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings';
import { CommandMenuMergeRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuMergeRecordPage';
@@ -55,8 +55,8 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
<CommandMenuPageLayoutGraphFilter />,
],
[
CommandMenuPages.PageLayoutIframeConfig,
<CommandMenuPageLayoutIframeConfig />,
CommandMenuPages.PageLayoutIframeSettings,
<CommandMenuPageLayoutIframeSettings />,
],
[
CommandMenuPages.PageLayoutTabSettings,
@@ -52,7 +52,7 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
const isPageLayoutEditingPage =
currentPage === CommandMenuPages.PageLayoutWidgetTypeSelect ||
currentPage === CommandMenuPages.PageLayoutGraphTypeSelect ||
currentPage === CommandMenuPages.PageLayoutIframeConfig ||
currentPage === CommandMenuPages.PageLayoutIframeSettings ||
currentPage === CommandMenuPages.PageLayoutTabSettings;
if (isPageLayoutEditingPage) {
@@ -1,152 +0,0 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { useCreatePageLayoutIframeWidget } from '@/page-layout/hooks/useCreatePageLayoutIframeWidget';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isString } from '@sniptt/guards';
import { useState } from 'react';
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 { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { createPageLayoutIframeWidget } =
useCreatePageLayoutIframeWidget(pageLayoutId);
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
useRecoilComponentState(
pageLayoutEditingWidgetIdComponentState,
pageLayoutId,
);
const pageLayoutDraft = useRecoilComponentValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
const editingWidget = allWidgets.find(
(w) => w.id === pageLayoutEditingWidgetId,
);
const isEditMode = !!editingWidget;
const [title, setTitle] = useState(editingWidget?.title || '');
const configUrl =
editingWidget?.configuration && 'url' in editingWidget.configuration
? editingWidget.configuration.url
: undefined;
const [url, setUrl] = useState(isString(configUrl) ? configUrl : '');
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) {
updatePageLayoutWidget(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 ? t`Edit iFrame Widget` : t`Configure iFrame Widget`}
</StyledSectionTitle>
<FormTextFieldInput
label={t`Widget Title`}
placeholder={t`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>
);
};
@@ -0,0 +1,103 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isNonEmptyString, isString } from '@sniptt/guards';
import { useState } from 'react';
import { isDefined, isValidUrl } from 'twenty-shared/utils';
import { IconFrame } from 'twenty-ui/display';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)};
padding: ${({ theme }) => theme.spacing(2)};
`;
export const CommandMenuPageLayoutIframeSettings = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const theme = useTheme();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
if (!isDefined(widgetInEditMode)) {
throw new Error('Widget ID must be present while editing the widget');
}
const configUrl =
widgetInEditMode.configuration && 'url' in widgetInEditMode.configuration
? widgetInEditMode.configuration.url
: null;
const [url, setUrl] = useState<string | null>(
isString(configUrl) ? configUrl : null,
);
const [urlError, setUrlError] = useState('');
const validateUrl = (urlString: string): boolean => {
const trimmedUrl = urlString.trim();
if (!isNonEmptyString(trimmedUrl)) {
setUrlError('');
return true;
}
if (!isValidUrl(trimmedUrl)) {
setUrlError(t`Please enter a valid URL`);
return false;
}
setUrlError('');
return true;
};
const handleUrlChange = (value: string) => {
setUrl(value);
if (validateUrl(value)) {
const trimmedValue = value.trim();
updatePageLayoutWidget(widgetInEditMode.id, {
configuration: {
...widgetInEditMode.configuration,
url: trimmedValue || null,
},
});
}
};
return (
<>
<SidePanelHeader
Icon={IconFrame}
iconColor={theme.font.color.tertiary}
initialTitle={widgetInEditMode.title}
headerType={t`iFrame Widget`}
onTitleChange={(newTitle) => {
if (!isNonEmptyString(newTitle)) {
return;
}
updatePageLayoutWidget(widgetInEditMode.id, {
title: newTitle,
});
}}
/>
<StyledContainer>
<FormTextFieldInput
label={t`URL to Embed`}
placeholder="https://example.com/embed"
defaultValue={url}
onChange={handleUrlChange}
error={urlError}
/>
</StyledContainer>
</>
);
};
@@ -6,6 +6,7 @@ import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pa
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useCompanyDefaultChartConfig } from '@/page-layout/hooks/useCompanyDefaultChartConfig';
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
import { useCreatePageLayoutIframeWidget } from '@/page-layout/hooks/useCreatePageLayoutIframeWidget';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
@@ -24,6 +25,9 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
const { createPageLayoutGraphWidget } =
useCreatePageLayoutGraphWidget(pageLayoutId);
const { createPageLayoutIframeWidget } =
useCreatePageLayoutIframeWidget(pageLayoutId);
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
useRecoilComponentState(
pageLayoutEditingWidgetIdComponentState,
@@ -46,9 +50,15 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
});
};
const handleNavigateToIframeConfig = () => {
const handleNavigateToIframeSettings = () => {
if (!isDefined(pageLayoutEditingWidgetId)) {
const newWidget = createPageLayoutIframeWidget('Untitled iFrame', null);
setPageLayoutEditingWidgetId(newWidget.id);
}
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutIframeConfig,
commandMenuPage: CommandMenuPages.PageLayoutIframeSettings,
});
};
@@ -68,15 +78,13 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
</SelectableListItem>
<SelectableListItem
itemId="iframe"
onEnter={() => {
handleNavigateToIframeConfig();
}}
onEnter={handleNavigateToIframeSettings}
>
<CommandMenuItem
Icon={IconFrame}
label="iFrame"
label={t`iFrame`}
id="iframe"
onClick={handleNavigateToIframeConfig}
onClick={handleNavigateToIframeSettings}
/>
</SelectableListItem>
</CommandGroup>
@@ -3,6 +3,6 @@ import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
export type PageLayoutCommandMenuPage =
| CommandMenuPages.PageLayoutWidgetTypeSelect
| CommandMenuPages.PageLayoutGraphTypeSelect
| CommandMenuPages.PageLayoutIframeConfig
| CommandMenuPages.PageLayoutIframeSettings
| CommandMenuPages.PageLayoutGraphFilter
| CommandMenuPages.PageLayoutTabSettings;
@@ -14,7 +14,7 @@ export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => {
return IconAppWindow;
case CommandMenuPages.PageLayoutGraphTypeSelect:
return IconChartPie;
case CommandMenuPages.PageLayoutIframeConfig:
case CommandMenuPages.PageLayoutIframeSettings:
return IconFrame;
case CommandMenuPages.PageLayoutGraphFilter:
return IconFilter;
@@ -9,8 +9,8 @@ export const getPageLayoutPageTitle = (page: PageLayoutCommandMenuPage) => {
return t`Add Widget`;
case CommandMenuPages.PageLayoutGraphTypeSelect:
return t`Select Graph Type`;
case CommandMenuPages.PageLayoutIframeConfig:
return t`Configure iFrame`;
case CommandMenuPages.PageLayoutIframeSettings:
return t`iFrame Settings`;
case CommandMenuPages.PageLayoutGraphFilter:
return t`Configure filters`;
case CommandMenuPages.PageLayoutTabSettings:
@@ -18,6 +18,6 @@ export enum CommandMenuPages {
PageLayoutWidgetTypeSelect = 'page-layout-widget-type-select',
PageLayoutGraphTypeSelect = 'page-layout-graph-type-select',
PageLayoutGraphFilter = 'page-layout-graph-filter',
PageLayoutIframeConfig = 'page-layout-iframe-config',
PageLayoutIframeSettings = 'page-layout-iframe-settings',
PageLayoutTabSettings = 'page-layout-tab-settings',
}
@@ -0,0 +1,29 @@
import { type WidgetSizeConfig } from '@/page-layout/types/WidgetSizeConfig';
import { GraphType } from '~/generated-metadata/graphql';
export const GRAPH_WIDGET_SIZES: Record<GraphType, WidgetSizeConfig> = {
[GraphType.AGGREGATE]: {
default: { w: 2, h: 2 },
minimum: { w: 2, h: 2 },
},
[GraphType.GAUGE]: {
default: { w: 3, h: 4 },
minimum: { w: 3, h: 4 },
},
[GraphType.PIE]: {
default: { w: 4, h: 4 },
minimum: { w: 3, h: 4 },
},
[GraphType.VERTICAL_BAR]: {
default: { w: 6, h: 6 },
minimum: { w: 4, h: 4 },
},
[GraphType.HORIZONTAL_BAR]: {
default: { w: 6, h: 6 },
minimum: { w: 4, h: 4 },
},
[GraphType.LINE]: {
default: { w: 6, h: 10 },
minimum: { w: 4, h: 4 },
},
};
@@ -1,33 +1,9 @@
import { GraphType } from '~/generated-metadata/graphql';
import { type WidgetSizeConfig } from '@/page-layout/types/WidgetSizeConfig';
import { WidgetType } from '~/generated/graphql';
type WidgetSizeConfig = {
default: { w: number; h: number };
minimum: { w: number; h: number };
};
export const WIDGET_SIZES: Record<GraphType, WidgetSizeConfig> = {
[GraphType.AGGREGATE]: {
default: { w: 2, h: 2 },
minimum: { w: 2, h: 2 },
},
[GraphType.GAUGE]: {
default: { w: 3, h: 4 },
minimum: { w: 3, h: 4 },
},
[GraphType.PIE]: {
default: { w: 4, h: 4 },
minimum: { w: 3, h: 4 },
},
[GraphType.VERTICAL_BAR]: {
export const WIDGET_SIZES: Partial<Record<WidgetType, WidgetSizeConfig>> = {
[WidgetType.IFRAME]: {
default: { w: 6, h: 6 },
minimum: { w: 4, h: 4 },
},
[GraphType.HORIZONTAL_BAR]: {
default: { w: 6, h: 6 },
minimum: { w: 4, h: 4 },
},
[GraphType.LINE]: {
default: { w: 6, h: 10 },
minimum: { w: 4, h: 4 },
minimum: { w: 4, h: 5 },
},
};
@@ -1,4 +1,4 @@
import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize';
import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -13,7 +13,9 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { type PageLayoutWidget, WidgetType } from '~/generated/graphql';
export const useCreatePageLayoutIframeWidget = (
pageLayoutIdFromProps?: string,
@@ -45,7 +47,7 @@ export const useCreatePageLayoutIframeWidget = (
const createPageLayoutIframeWidget = useRecoilCallback(
({ snapshot, set }) =>
(title: string, url: string) => {
(title: string, url: string | null): PageLayoutWidget => {
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
@@ -54,13 +56,16 @@ export const useCreatePageLayoutIframeWidget = (
.getLoadable(pageLayoutDraggedAreaState)
.getValue();
if (!activeTabId) {
return;
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new iframe widget',
);
}
const widgetId = uuidv4();
const defaultIframeSize = { w: 6, h: 6 };
const minimumSize = DEFAULT_WIDGET_SIZE.minimum;
const iframeSize = WIDGET_SIZES[WidgetType.IFRAME]!;
const defaultIframeSize = iframeSize.default;
const minimumSize = iframeSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultIframeSize,
@@ -104,6 +109,8 @@ export const useCreatePageLayoutIframeWidget = (
}));
set(pageLayoutDraggedAreaState, null);
return newWidget;
},
[
activeTabId,
@@ -35,7 +35,7 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
if (widgetType === WidgetType.IFRAME) {
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutIframeConfig,
commandMenuPage: CommandMenuPages.PageLayoutIframeSettings,
pageTitle: t`Edit iFrame`,
});
}
@@ -0,0 +1,4 @@
export type WidgetSizeConfig = {
default: { w: number; h: number };
minimum: { w: number; h: number };
};
@@ -1,4 +1,5 @@
import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize';
import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
import { getWidgetSize } from '@/page-layout/utils/getWidgetSize';
@@ -32,6 +33,12 @@ export const convertPageLayoutToTabLayouts = (
}
}
if (widget.type === WidgetType.IFRAME) {
const iframeMinimumSize = WIDGET_SIZES[WidgetType.IFRAME]!.minimum;
minW = iframeMinimumSize.w;
minH = iframeMinimumSize.h;
}
return {
i: widget.id,
x: widget.gridPosition.column,
@@ -8,7 +8,7 @@ export const createDefaultIframeWidget = (
id: string,
pageLayoutTabId: string,
title: string,
url: string,
url: string | null,
gridPosition: GridPosition,
objectMetadataId?: string | null,
): PageLayoutWidget => {
@@ -1,11 +1,11 @@
import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize';
import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes';
import { GRAPH_WIDGET_SIZES } from '@/page-layout/constants/GraphWidgetSizes';
import { type GraphType } from '~/generated/graphql';
export const getWidgetSize = (
graphType: GraphType,
type: 'default' | 'minimum',
): { w: number; h: number } => {
const sizeConfig = WIDGET_SIZES[graphType] ?? DEFAULT_WIDGET_SIZE;
const sizeConfig = GRAPH_WIDGET_SIZES[graphType] ?? DEFAULT_WIDGET_SIZE;
return sizeConfig[type];
};
@@ -1,24 +1,30 @@
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { type PageLayoutWidget } from '~/generated-metadata/graphql';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutWidget } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.transparent.lighter};
const StyledContainer = styled.div<{ $isEditMode: boolean }>`
border-radius: ${({ theme }) => theme.border.radius.md};
background: ${({ theme }) => theme.background.primary};
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
pointer-events: ${({ $isEditMode }) => ($isEditMode ? 'none' : 'auto')};
`;
const StyledIframe = styled.iframe`
const StyledIframe = styled.iframe<{ $isEditMode: boolean }>`
border: none;
flex: 1;
height: 100%;
width: 100%;
pointer-events: ${({ $isEditMode }) => ($isEditMode ? 'none' : 'auto')};
`;
const StyledLoadingContainer = styled.div`
@@ -44,30 +50,19 @@ const StyledErrorContainer = styled.div`
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 = {
widget: PageLayoutWidget;
};
export const IframeWidget = ({ widget }: IframeWidgetProps) => {
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
const configuration = widget.configuration;
if (!configuration || !('url' in configuration)) {
throw new Error(
`Invalid configuration for widget ${widget.id}: missing url`,
);
throw new Error(`Invalid configuration for widget ${widget.id}`);
}
const url = configuration.url;
@@ -85,25 +80,25 @@ export const IframeWidget = ({ widget }: IframeWidgetProps) => {
setHasError(true);
};
if (hasError) {
if (hasError || !isDefined(url)) {
return (
<StyledContainer>
<StyledContainer $isEditMode={isPageLayoutInEditMode}>
<StyledErrorContainer>
<StyledErrorMessage>Failed to load content</StyledErrorMessage>
<StyledErrorUrl>{url}</StyledErrorUrl>
<PageLayoutWidgetNoDataDisplay widgetId={widget.id} />
</StyledErrorContainer>
</StyledContainer>
);
}
return (
<StyledContainer>
<StyledContainer $isEditMode={isPageLayoutInEditMode}>
{isLoading && (
<StyledLoadingContainer>
<ChartSkeletonLoader />
</StyledLoadingContainer>
)}
<StyledIframe
$isEditMode={isPageLayoutInEditMode}
src={url}
title={title}
onLoad={handleIframeLoad}