diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultWidgetSize.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultWidgetSize.ts new file mode 100644 index 0000000000..3f26352bd7 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultWidgetSize.ts @@ -0,0 +1,4 @@ +export const DEFAULT_WIDGET_SIZE = { + default: { w: 4, h: 4 }, + minimum: { w: 2, h: 2 }, +}; diff --git a/packages/twenty-front/src/modules/page-layout/constants/WidgetSizes.ts b/packages/twenty-front/src/modules/page-layout/constants/WidgetSizes.ts new file mode 100644 index 0000000000..aa1e1bfdc0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/constants/WidgetSizes.ts @@ -0,0 +1,33 @@ +import { GraphType } from '~/generated-metadata/graphql'; + +type WidgetSizeConfig = { + default: { w: number; h: number }; + minimum: { w: number; h: number }; +}; + +export const WIDGET_SIZES: Record = { + [GraphType.NUMBER]: { + default: { w: 3, 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: 5, h: 5 }, + }, + [GraphType.HORIZONTAL_BAR]: { + default: { w: 6, h: 6 }, + minimum: { w: 5, h: 5 }, + }, + [GraphType.LINE]: { + default: { w: 6, h: 10 }, + minimum: { w: 5, h: 5 }, + }, +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts index 49addd55ef..594b0b73c0 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts @@ -5,13 +5,11 @@ import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLa import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget'; -import { - getWidgetSize, - getWidgetTitle, -} from '@/page-layout/utils/getDefaultWidgetData'; import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition'; import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts'; +import { getWidgetSize } from '@/page-layout/utils/getWidgetSize'; +import { getWidgetTitle } from '@/page-layout/utils/getWidgetTitle'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; @@ -91,10 +89,12 @@ export const useCreatePageLayoutGraphWidget = ( const title = getWidgetTitle(graphType, existingWidgetCount); const widgetId = uuidv4(); - const defaultSize = getWidgetSize(graphType); + const defaultSize = getWidgetSize(graphType, 'default'); + const minimumSize = getWidgetSize(graphType, 'minimum'); const position = getDefaultWidgetPosition( pageLayoutDraggedArea, defaultSize, + minimumSize, ); const newWidget = createDefaultGraphWidget({ diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts index 777bdfd5c5..a29b07b231 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts @@ -1,3 +1,4 @@ +import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; @@ -59,9 +60,11 @@ export const useCreatePageLayoutIframeWidget = ( const widgetId = uuidv4(); const defaultSize = { w: 6, h: 6 }; + const minimumSize = DEFAULT_WIDGET_SIZE.minimum; const position = getDefaultWidgetPosition( pageLayoutDraggedArea, defaultSize, + minimumSize, ); const newWidget = createDefaultIframeWidget( diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts index 06caca48fc..01b946a188 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts @@ -76,12 +76,12 @@ describe('convertPageLayoutToTabLayouts', () => { expect(result).toEqual({ 'tab-1': { desktop: [ - { i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }, - { i: 'widget-2', x: 0, y: 2, w: 2, h: 2 }, + { i: 'widget-1', x: 0, y: 0, w: 2, h: 2, minW: 2, minH: 2 }, + { i: 'widget-2', x: 0, y: 2, w: 2, h: 2, minW: 3, minH: 4 }, ], mobile: [ - { i: 'widget-1', x: 0, y: 0, w: 1, h: 2 }, - { i: 'widget-2', x: 0, y: 2, w: 1, h: 2 }, + { i: 'widget-1', x: 0, y: 0, w: 1, h: 2, minW: 2, minH: 2 }, + { i: 'widget-2', x: 0, y: 2, w: 1, h: 2, minW: 3, minH: 4 }, ], }, }); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getDefaultWidgetPosition.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getDefaultWidgetPosition.test.ts index e74301769d..d97f01fefd 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getDefaultWidgetPosition.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getDefaultWidgetPosition.test.ts @@ -1,19 +1,66 @@ import { getDefaultWidgetPosition } from '../getDefaultWidgetPosition'; describe('getDefaultWidgetPosition', () => { - it('should return dragged area when provided', () => { + it('should return dragged area when it meets minimum size', () => { const draggedArea = { x: 2, y: 3, w: 4, h: 5 }; const defaultSize = { w: 2, h: 2 }; + const minimumSize = { w: 3, h: 3 }; - expect(getDefaultWidgetPosition(draggedArea, defaultSize)).toEqual( - draggedArea, - ); + expect( + getDefaultWidgetPosition(draggedArea, defaultSize, minimumSize), + ).toEqual(draggedArea); + }); + + it('should expand dragged area to meet minimum width', () => { + const draggedArea = { x: 2, y: 3, w: 2, h: 5 }; + const defaultSize = { w: 4, h: 4 }; + const minimumSize = { w: 5, h: 3 }; + + expect( + getDefaultWidgetPosition(draggedArea, defaultSize, minimumSize), + ).toEqual({ + x: 2, + y: 3, + w: 5, // expanded from 2 to meet minimum + h: 5, + }); + }); + + it('should expand dragged area to meet minimum height', () => { + const draggedArea = { x: 1, y: 2, w: 6, h: 2 }; + const defaultSize = { w: 4, h: 4 }; + const minimumSize = { w: 3, h: 5 }; + + expect( + getDefaultWidgetPosition(draggedArea, defaultSize, minimumSize), + ).toEqual({ + x: 1, + y: 2, + w: 6, + h: 5, // expanded from 2 to meet minimum + }); + }); + + it('should expand dragged area to meet both minimum dimensions', () => { + const draggedArea = { x: 0, y: 0, w: 2, h: 2 }; + const defaultSize = { w: 6, h: 6 }; + const minimumSize = { w: 5, h: 5 }; + + expect( + getDefaultWidgetPosition(draggedArea, defaultSize, minimumSize), + ).toEqual({ + x: 0, + y: 0, + w: 5, // expanded from 2 to meet minimum + h: 5, // expanded from 2 to meet minimum + }); }); it('should return default position with size when no dragged area', () => { const defaultSize = { w: 3, h: 4 }; + const minimumSize = { w: 2, h: 2 }; - expect(getDefaultWidgetPosition(null, defaultSize)).toEqual({ + expect(getDefaultWidgetPosition(null, defaultSize, minimumSize)).toEqual({ x: 0, y: 0, w: 3, diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts index 41c14469af..3068c35d34 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts @@ -1,5 +1,9 @@ +import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize'; import { type PageLayout } from '@/page-layout/types/PageLayout'; import { type TabLayouts } from '@/page-layout/types/tab-layouts'; +import { getWidgetSize } from '@/page-layout/utils/getWidgetSize'; +import { isDefined } from 'twenty-shared/utils'; +import { WidgetType } from '~/generated/graphql'; export const convertPageLayoutToTabLayouts = ( pageLayout: PageLayout, @@ -11,13 +15,33 @@ export const convertPageLayoutToTabLayouts = ( const tabLayouts: TabLayouts = {}; pageLayout.tabs.forEach((tab) => { - const layouts = tab.widgets.map((widget) => ({ - i: widget.id, - x: widget.gridPosition.column, - y: widget.gridPosition.row, - w: widget.gridPosition.columnSpan, - h: widget.gridPosition.rowSpan, - })); + const layouts = tab.widgets.map((widget) => { + let minW = DEFAULT_WIDGET_SIZE.minimum.w; + let minH = DEFAULT_WIDGET_SIZE.minimum.h; + + if (widget.type === WidgetType.GRAPH && isDefined(widget.configuration)) { + const graphType = + 'graphType' in widget.configuration + ? widget.configuration.graphType + : undefined; + + if (isDefined(graphType)) { + const minimumSize = getWidgetSize(graphType, 'minimum'); + minW = minimumSize.w; + minH = minimumSize.h; + } + } + + return { + i: widget.id, + x: widget.gridPosition.column, + y: widget.gridPosition.row, + w: widget.gridPosition.columnSpan, + h: widget.gridPosition.rowSpan, + minW, + minH, + }; + }); tabLayouts[tab.id] = { desktop: layouts, diff --git a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts index 1b9859f802..9c2c78a1e1 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts @@ -128,34 +128,3 @@ export const getDefaultWidgetData = (graphType: GraphType) => { return {}; } }; - -export const getWidgetTitle = (graphType: GraphType, index: number): string => { - const baseNames: Record = { - [GraphType.NUMBER]: 'Number', - [GraphType.GAUGE]: 'Gauge', - [GraphType.PIE]: 'Pie Chart', - [GraphType.VERTICAL_BAR]: 'Vertical Bar Chart', - [GraphType.HORIZONTAL_BAR]: 'Horizontal Bar Chart', - [GraphType.LINE]: 'Line Chart', - }; - - return `${baseNames[graphType] || 'Widget'} ${index + 1}`; -}; - -export const getWidgetSize = (graphType: GraphType) => { - switch (graphType) { - case GraphType.NUMBER: - return { w: 3, h: 2 }; - case GraphType.GAUGE: - return { w: 3, h: 3 }; - case GraphType.PIE: - return { w: 4, h: 4 }; - case GraphType.VERTICAL_BAR: - case GraphType.HORIZONTAL_BAR: - return { w: 6, h: 6 }; - case GraphType.LINE: - return { w: 6, h: 10 }; - default: - return { w: 4, h: 4 }; - } -}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetPosition.ts b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetPosition.ts index 3fe5aff634..a238466f76 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetPosition.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetPosition.ts @@ -3,9 +3,15 @@ import { type GridBounds } from './calculateGridBoundsFromSelectedCells'; export const getDefaultWidgetPosition = ( draggedArea: GridBounds | null, defaultSize: { w: number; h: number }, + minimumSize: { w: number; h: number }, ): GridBounds => { if (draggedArea !== null) { - return draggedArea; + return { + x: draggedArea.x, + y: draggedArea.y, + w: Math.max(draggedArea.w, minimumSize.w), + h: Math.max(draggedArea.h, minimumSize.h), + }; } return { diff --git a/packages/twenty-front/src/modules/page-layout/utils/getWidgetSize.ts b/packages/twenty-front/src/modules/page-layout/utils/getWidgetSize.ts new file mode 100644 index 0000000000..0b8366cbd9 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/getWidgetSize.ts @@ -0,0 +1,11 @@ +import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize'; +import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes'; +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; + return sizeConfig[type]; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/getWidgetTitle.ts b/packages/twenty-front/src/modules/page-layout/utils/getWidgetTitle.ts new file mode 100644 index 0000000000..ca684d1b49 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/getWidgetTitle.ts @@ -0,0 +1,14 @@ +import { GraphType } from '~/generated/graphql'; + +export const getWidgetTitle = (graphType: GraphType, index: number): string => { + const baseNames: Record = { + [GraphType.NUMBER]: 'Number', + [GraphType.GAUGE]: 'Gauge', + [GraphType.PIE]: 'Pie Chart', + [GraphType.VERTICAL_BAR]: 'Vertical Bar Chart', + [GraphType.HORIZONTAL_BAR]: 'Horizontal Bar Chart', + [GraphType.LINE]: 'Line Chart', + }; + + return `${baseNames[graphType] || 'Widget'} ${index + 1}`; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx index f1ae063172..0b7f1739b9 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx @@ -51,6 +51,7 @@ export const WidgetHeader = ({ variant="tertiary" size="small" disabled={isEmpty} + onClick={(e) => e.stopPropagation()} /> )} {isEmpty ? 'Add Widget' : title}