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:
@@ -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 },
|
||||
},
|
||||
};
|
||||
|
||||
+13
-6
@@ -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];
|
||||
};
|
||||
|
||||
+20
-25
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user