Instant widget placeholder placement on drag end (#15496)
closes - https://discord.com/channels/1130383047699738754/1430596561137700957 fixed a bug - where on chart type change in side panel -- wont update the widgets minimum size --------- Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
+7
@@ -15,6 +15,7 @@ import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
@@ -85,6 +86,12 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
|
||||
}),
|
||||
null,
|
||||
);
|
||||
set(
|
||||
pageLayoutDraggedAreaComponentState.atomFamily({
|
||||
instanceId: record.pageLayoutId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-35
@@ -5,13 +5,13 @@ import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateComm
|
||||
import { ChartSettingItem } from '@/command-menu/pages/page-layout/components/chart-settings/ChartSettingItem';
|
||||
import { ChartTypeSelectionSection } from '@/command-menu/pages/page-layout/components/ChartTypeSelectionSection';
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { GRAPH_TYPE_TO_CONFIG_TYPENAME } from '@/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename';
|
||||
import { useChartSettingsValues } from '@/command-menu/pages/page-layout/hooks/useChartSettingsValues';
|
||||
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateChartSettingInput } from '@/command-menu/pages/page-layout/hooks/useUpdateChartSettingInput';
|
||||
import { useUpdateChartSettingToggle } from '@/command-menu/pages/page-layout/hooks/useUpdateChartSettingToggle';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useUpdateGraphTypeConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateGraphTypeConfig';
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { shouldHideChartSetting } from '@/command-menu/pages/page-layout/utils/shouldHideChartSetting';
|
||||
@@ -24,14 +24,9 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import { SidePanelInformationBanner } from 'twenty-ui/display';
|
||||
|
||||
import {
|
||||
AggregateOperations,
|
||||
GraphType,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated/graphql';
|
||||
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
const StyledSidePanelInformationBanner = styled(SidePanelInformationBanner)`
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
@@ -69,6 +64,12 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
|
||||
|
||||
const { updateChartSettingInput } = useUpdateChartSettingInput(pageLayoutId);
|
||||
|
||||
const { updateGraphTypeConfig } = useUpdateGraphTypeConfig({
|
||||
pageLayoutId,
|
||||
widget,
|
||||
configuration,
|
||||
});
|
||||
|
||||
const isGroupByEnabled = getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
|
||||
);
|
||||
@@ -76,34 +77,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
|
||||
useRecoilComponentState(hasWidgetTooManyGroupsComponentState);
|
||||
|
||||
const handleGraphTypeChange = (graphType: GraphType) => {
|
||||
const configToUpdate: Record<string, any> = {
|
||||
__typename: GRAPH_TYPE_TO_CONFIG_TYPENAME[graphType],
|
||||
graphType,
|
||||
};
|
||||
|
||||
if (graphType !== GraphType.AGGREGATE && graphType !== GraphType.GAUGE) {
|
||||
const currentAggregateFieldMetadataId =
|
||||
configuration.aggregateFieldMetadataId;
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widget.objectMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
const aggregateField = objectMetadataItem.fields.find(
|
||||
(field) => field.id === currentAggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(aggregateField) &&
|
||||
isFieldMetadataDateKind(aggregateField.type) &&
|
||||
(configuration.aggregateOperation === AggregateOperations.MIN ||
|
||||
configuration.aggregateOperation === AggregateOperations.MAX)
|
||||
) {
|
||||
configToUpdate.aggregateOperation = AggregateOperations.COUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
const configToUpdate = updateGraphTypeConfig(graphType);
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate,
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { GRAPH_TYPE_TO_CONFIG_TYPENAME } from '@/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename';
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
|
||||
import { updateWidgetMinimumSizeForGraphType } from '@/page-layout/utils/updateWidgetMinimumSizeForGraphType';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
export const useUpdateGraphTypeConfig = ({
|
||||
pageLayoutId,
|
||||
widget,
|
||||
configuration,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
widget: PageLayoutWidget;
|
||||
configuration: ChartConfiguration;
|
||||
}) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
|
||||
|
||||
const activeTabIdState = useRecoilComponentCallbackState(
|
||||
activeTabIdComponentState,
|
||||
tabListInstanceId,
|
||||
);
|
||||
|
||||
const currentlyEditingWidgetIdState = useRecoilComponentCallbackState(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
|
||||
pageLayoutCurrentLayoutsComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const updateGraphTypeConfig = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(graphType: GraphType) => {
|
||||
const configToUpdate: Record<string, any> = {
|
||||
__typename: GRAPH_TYPE_TO_CONFIG_TYPENAME[graphType],
|
||||
graphType,
|
||||
};
|
||||
|
||||
if (
|
||||
graphType !== GraphType.AGGREGATE &&
|
||||
graphType !== GraphType.GAUGE
|
||||
) {
|
||||
const currentAggregateFieldMetadataId =
|
||||
configuration.aggregateFieldMetadataId;
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widget.objectMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
const aggregateField = objectMetadataItem.fields.find(
|
||||
(field) => field.id === currentAggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(aggregateField) &&
|
||||
isFieldMetadataDateKind(aggregateField.type) &&
|
||||
(configuration.aggregateOperation === AggregateOperations.MIN ||
|
||||
configuration.aggregateOperation === AggregateOperations.MAX)
|
||||
) {
|
||||
configToUpdate.aggregateOperation = AggregateOperations.COUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeTabId = snapshot.getLoadable(activeTabIdState).getValue();
|
||||
const currentlyEditingWidgetId = snapshot
|
||||
.getLoadable(currentlyEditingWidgetIdState)
|
||||
.getValue();
|
||||
|
||||
if (isDefined(activeTabId) && isDefined(currentlyEditingWidgetId)) {
|
||||
const currentLayouts = snapshot
|
||||
.getLoadable(pageLayoutCurrentLayoutsState)
|
||||
.getValue();
|
||||
|
||||
const updatedLayouts = updateWidgetMinimumSizeForGraphType(
|
||||
graphType,
|
||||
currentlyEditingWidgetId,
|
||||
activeTabId,
|
||||
currentLayouts,
|
||||
);
|
||||
|
||||
set(pageLayoutCurrentLayoutsState, updatedLayouts);
|
||||
}
|
||||
|
||||
return configToUpdate;
|
||||
},
|
||||
[
|
||||
activeTabIdState,
|
||||
configuration,
|
||||
currentlyEditingWidgetIdState,
|
||||
objectMetadataItems,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
widget.objectMetadataId,
|
||||
],
|
||||
);
|
||||
|
||||
return { updateGraphTypeConfig };
|
||||
};
|
||||
@@ -11,16 +11,22 @@ import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayo
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/pageLayoutCurrentBreakpointComponentState';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
|
||||
import { addPendingPlaceholderToLayouts } from '@/page-layout/utils/addPendingPlaceholderToLayouts';
|
||||
import { filterPendingPlaceholderFromLayouts } from '@/page-layout/utils/filterPendingPlaceholderFromLayouts';
|
||||
import { prepareGridLayoutItemsWithPlaceholders } from '@/page-layout/utils/prepareGridLayoutItemsWithPlaceholders';
|
||||
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
|
||||
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import {
|
||||
Responsive,
|
||||
WidthProvider,
|
||||
type Layout,
|
||||
type Layouts,
|
||||
type ResponsiveProps,
|
||||
} from 'react-grid-layout';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -74,6 +80,16 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
|
||||
|
||||
const { handleLayoutChange } = usePageLayoutHandleLayoutChange();
|
||||
|
||||
const handleLayoutChangeWithoutPendingPlaceholder = (
|
||||
currentLayout: Layout[],
|
||||
allLayouts: Layouts,
|
||||
) => {
|
||||
handleLayoutChange(
|
||||
currentLayout,
|
||||
filterPendingPlaceholderFromLayouts(allLayouts),
|
||||
);
|
||||
};
|
||||
|
||||
const gridContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
@@ -84,23 +100,43 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
|
||||
pageLayoutCurrentLayoutsComponentState,
|
||||
);
|
||||
|
||||
const pageLayoutDraggedArea = useRecoilComponentValue(
|
||||
pageLayoutDraggedAreaComponentState,
|
||||
);
|
||||
|
||||
const { currentPageLayout } = useCurrentPageLayout();
|
||||
|
||||
const activeTab = currentPageLayout?.tabs.find((tab) => tab.id === tabId);
|
||||
|
||||
if (!isDefined(currentPageLayout) || !isDefined(activeTab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTabWidgets = activeTab.widgets;
|
||||
const activeTabWidgets = activeTab?.widgets;
|
||||
|
||||
const isLayoutEmpty =
|
||||
!isDefined(activeTabWidgets) || activeTabWidgets.length === 0;
|
||||
|
||||
const layouts = isLayoutEmpty
|
||||
const hasPendingPlaceholder =
|
||||
isDefined(pageLayoutDraggedArea) && !isLayoutEmpty;
|
||||
|
||||
const baseLayouts = isLayoutEmpty
|
||||
? EMPTY_LAYOUT
|
||||
: (pageLayoutCurrentLayouts[tabId] ?? EMPTY_LAYOUT);
|
||||
|
||||
const layouts = hasPendingPlaceholder
|
||||
? addPendingPlaceholderToLayouts(baseLayouts, pageLayoutDraggedArea)
|
||||
: baseLayouts;
|
||||
|
||||
const gridLayoutItems = useMemo(
|
||||
() =>
|
||||
prepareGridLayoutItemsWithPlaceholders(
|
||||
activeTabWidgets,
|
||||
hasPendingPlaceholder,
|
||||
),
|
||||
[activeTabWidgets, hasPendingPlaceholder],
|
||||
);
|
||||
|
||||
if (!isDefined(currentPageLayout) || !isDefined(activeTab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledGridContainer ref={gridContainerRef}>
|
||||
@@ -137,28 +173,26 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
|
||||
onDragStop={() => {
|
||||
setDraggingWidgetId(null);
|
||||
}}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
onLayoutChange={handleLayoutChangeWithoutPendingPlaceholder}
|
||||
onBreakpointChange={(newBreakpoint) =>
|
||||
setPageLayoutCurrentBreakpoint(
|
||||
newBreakpoint as PageLayoutBreakpoint,
|
||||
)
|
||||
}
|
||||
>
|
||||
{isLayoutEmpty ? (
|
||||
<div key="empty-placeholder" data-select-disable="true">
|
||||
<WidgetPlaceholder />
|
||||
</div>
|
||||
) : (
|
||||
activeTabWidgets?.map((widget) => (
|
||||
<div key={widget.id} data-select-disable="true">
|
||||
{gridLayoutItems.map((item) => (
|
||||
<div key={item.id} data-select-disable="true">
|
||||
{item.type === 'placeholder' ? (
|
||||
<WidgetPlaceholder />
|
||||
) : (
|
||||
<WidgetRenderer
|
||||
widget={widget}
|
||||
widget={item.widget}
|
||||
pageLayoutType={currentPageLayout.type}
|
||||
layoutMode="grid"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
</StyledGridContainer>
|
||||
</>
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY =
|
||||
'__pending-widget-placeholder__';
|
||||
@@ -0,0 +1,4 @@
|
||||
export const WIDGET_PLACEHOLDER_SIZE = {
|
||||
w: 4,
|
||||
h: 4,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
export type GridLayoutWidgetItem = {
|
||||
id: string;
|
||||
type: 'widget';
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export type GridLayoutPlaceholderItem = {
|
||||
id: string;
|
||||
type: 'placeholder';
|
||||
};
|
||||
|
||||
export type GridLayoutItem = GridLayoutWidgetItem | GridLayoutPlaceholderItem;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { calculateGridCellPosition } from '../calculateGridCellPosition';
|
||||
|
||||
describe('calculateGridCellPosition', () => {
|
||||
it('should calculate correct position for first cell', () => {
|
||||
const result = calculateGridCellPosition({
|
||||
index: 0,
|
||||
numberOfColumns: 4,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ row: 0, column: 0 });
|
||||
});
|
||||
|
||||
it('should calculate correct position for cells in first row', () => {
|
||||
const result = calculateGridCellPosition({
|
||||
index: 3,
|
||||
numberOfColumns: 4,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ row: 0, column: 3 });
|
||||
});
|
||||
|
||||
it('should calculate correct position for cells in subsequent rows', () => {
|
||||
const result = calculateGridCellPosition({
|
||||
index: 5,
|
||||
numberOfColumns: 4,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ row: 1, column: 1 });
|
||||
});
|
||||
|
||||
it('should handle different column counts', () => {
|
||||
const result2Columns = calculateGridCellPosition({
|
||||
index: 3,
|
||||
numberOfColumns: 2,
|
||||
});
|
||||
expect(result2Columns).toEqual({ row: 1, column: 1 });
|
||||
|
||||
const result6Columns = calculateGridCellPosition({
|
||||
index: 7,
|
||||
numberOfColumns: 6,
|
||||
});
|
||||
expect(result6Columns).toEqual({ row: 1, column: 1 });
|
||||
});
|
||||
|
||||
it('should handle large indices', () => {
|
||||
const result = calculateGridCellPosition({
|
||||
index: 99,
|
||||
numberOfColumns: 10,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ row: 9, column: 9 });
|
||||
});
|
||||
|
||||
it('should handle single column layout', () => {
|
||||
const result = calculateGridCellPosition({
|
||||
index: 5,
|
||||
numberOfColumns: 1,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ row: 5, column: 0 });
|
||||
});
|
||||
});
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
AggregateOperations,
|
||||
AxisNameDisplay,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '~/generated/graphql';
|
||||
import { createDefaultGraphWidget } from '../createDefaultGraphWidget';
|
||||
|
||||
describe('createDefaultGraphWidget', () => {
|
||||
const baseParams = {
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Test Widget',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: 'object-1',
|
||||
};
|
||||
|
||||
describe('AGGREGATE graph type', () => {
|
||||
it('should create widget with aggregate configuration when field is provided', () => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType: GraphType.AGGREGATE,
|
||||
fieldSelection: {
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(widget.type).toBe(WidgetType.GRAPH);
|
||||
expect(widget.configuration).toEqual({
|
||||
__typename: 'AggregateChartConfiguration',
|
||||
graphType: GraphType.AGGREGATE,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
displayDataLabel: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null configuration when field is missing', () => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType: GraphType.AGGREGATE,
|
||||
});
|
||||
|
||||
expect(widget.configuration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('VERTICAL_BAR graph type', () => {
|
||||
it('should create widget with bar chart configuration when all fields are provided', () => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
fieldSelection: {
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(widget.configuration).toEqual({
|
||||
__typename: 'BarChartConfiguration',
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
displayDataLabel: false,
|
||||
color: 'blue',
|
||||
primaryAxisGroupByFieldMetadataId: 'field-2',
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
axisNameDisplay: AxisNameDisplay.NONE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null configuration when required fields are missing', () => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
fieldSelection: {
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
// Missing groupByFieldMetadataIdX
|
||||
},
|
||||
});
|
||||
|
||||
expect(widget.configuration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('HORIZONTAL_BAR graph type', () => {
|
||||
it('should create widget with horizontal bar configuration', () => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
fieldSelection: {
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(widget.configuration?.__typename).toBe('BarChartConfiguration');
|
||||
if (widget.configuration?.__typename === 'BarChartConfiguration') {
|
||||
expect(widget.configuration.graphType).toBe(GraphType.HORIZONTAL_BAR);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unsupported graph types', () => {
|
||||
it.each([GraphType.PIE, GraphType.LINE, GraphType.GAUGE])(
|
||||
'should return null configuration for %s graph type',
|
||||
(graphType) => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType,
|
||||
fieldSelection: {
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(widget.configuration).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use objectMetadataId from fieldSelection over params', () => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType: GraphType.AGGREGATE,
|
||||
objectMetadataId: 'object-1',
|
||||
fieldSelection: {
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
objectMetadataId: 'object-2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(widget.objectMetadataId).toBe('object-2');
|
||||
});
|
||||
|
||||
it('should set timestamps and common widget properties', () => {
|
||||
const widget = createDefaultGraphWidget({
|
||||
...baseParams,
|
||||
graphType: GraphType.AGGREGATE,
|
||||
fieldSelection: {
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(widget.__typename).toBe('PageLayoutWidget');
|
||||
expect(widget.id).toBe('widget-1');
|
||||
expect(widget.pageLayoutTabId).toBe('tab-1');
|
||||
expect(widget.title).toBe('Test Widget');
|
||||
expect(widget.gridPosition).toEqual(baseParams.gridPosition);
|
||||
expect(widget.createdAt).toBeDefined();
|
||||
expect(widget.updatedAt).toBeDefined();
|
||||
expect(widget.deletedAt).toBeNull();
|
||||
});
|
||||
});
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize';
|
||||
import { PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY } from '@/page-layout/constants/PendingWidgetPlaceholderLayoutKey';
|
||||
import { createPendingWidgetPlaceholderLayoutItem } from '../createPendingWidgetPlaceholderLayoutItem';
|
||||
|
||||
describe('createPendingWidgetPlaceholderLayoutItem', () => {
|
||||
const draggedArea = { x: 2, y: 3, w: 5, h: 6 };
|
||||
|
||||
describe('desktop breakpoint', () => {
|
||||
it('should use dragged area position and apply minimum constraints', () => {
|
||||
const result = createPendingWidgetPlaceholderLayoutItem(
|
||||
draggedArea,
|
||||
'desktop',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
x: 2,
|
||||
y: 3,
|
||||
w: 5,
|
||||
h: 6,
|
||||
minW: DEFAULT_WIDGET_SIZE.minimum.w,
|
||||
minH: DEFAULT_WIDGET_SIZE.minimum.h,
|
||||
static: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply default size when dragged area is smaller', () => {
|
||||
const smallDraggedArea = { x: 1, y: 2, w: 1, h: 1 };
|
||||
const result = createPendingWidgetPlaceholderLayoutItem(
|
||||
smallDraggedArea,
|
||||
'desktop',
|
||||
);
|
||||
|
||||
expect(result.w).toBe(DEFAULT_WIDGET_SIZE.default.w);
|
||||
expect(result.h).toBe(DEFAULT_WIDGET_SIZE.default.h);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mobile breakpoint', () => {
|
||||
it('should always use x=0 and w=1 for mobile', () => {
|
||||
const result = createPendingWidgetPlaceholderLayoutItem(
|
||||
draggedArea,
|
||||
'mobile',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
x: 0,
|
||||
y: 3,
|
||||
w: 1,
|
||||
h: 6,
|
||||
minW: DEFAULT_WIDGET_SIZE.minimum.w,
|
||||
minH: DEFAULT_WIDGET_SIZE.minimum.h,
|
||||
static: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should still apply minimum height for mobile', () => {
|
||||
const smallDraggedArea = { x: 5, y: 2, w: 10, h: 1 };
|
||||
const result = createPendingWidgetPlaceholderLayoutItem(
|
||||
smallDraggedArea,
|
||||
'mobile',
|
||||
);
|
||||
|
||||
expect(result.x).toBe(0);
|
||||
expect(result.w).toBe(1);
|
||||
expect(result.h).toBe(DEFAULT_WIDGET_SIZE.default.h);
|
||||
});
|
||||
});
|
||||
|
||||
it('should always mark layout as static', () => {
|
||||
const desktopResult = createPendingWidgetPlaceholderLayoutItem(
|
||||
draggedArea,
|
||||
'desktop',
|
||||
);
|
||||
const mobileResult = createPendingWidgetPlaceholderLayoutItem(
|
||||
draggedArea,
|
||||
'mobile',
|
||||
);
|
||||
|
||||
expect(desktopResult.static).toBe(true);
|
||||
expect(mobileResult.static).toBe(true);
|
||||
});
|
||||
});
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY } from '@/page-layout/constants/PendingWidgetPlaceholderLayoutKey';
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { filterPendingPlaceholderFromLayouts } from '../filterPendingPlaceholderFromLayouts';
|
||||
|
||||
describe('filterPendingPlaceholderFromLayouts', () => {
|
||||
it('should remove pending placeholder from both desktop and mobile layouts', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 4, y: 0, w: 4, h: 4 },
|
||||
{ i: 'widget-2', x: 8, y: 0, w: 4, h: 4 },
|
||||
],
|
||||
mobile: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 1, h: 4 },
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 0, y: 4, w: 1, h: 4 },
|
||||
{ i: 'widget-2', x: 0, y: 8, w: 1, h: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = filterPendingPlaceholderFromLayouts(layouts);
|
||||
|
||||
expect(result.desktop).toHaveLength(2);
|
||||
expect(result.desktop).toEqual([
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: 'widget-2', x: 8, y: 0, w: 4, h: 4 },
|
||||
]);
|
||||
|
||||
expect(result.mobile).toHaveLength(2);
|
||||
expect(result.mobile).toEqual([
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 1, h: 4 },
|
||||
{ i: 'widget-2', x: 0, y: 8, w: 1, h: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle layouts with no pending placeholder', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: 'widget-2', x: 4, y: 0, w: 4, h: 4 },
|
||||
],
|
||||
mobile: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 1, h: 4 },
|
||||
{ i: 'widget-2', x: 0, y: 4, w: 1, h: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = filterPendingPlaceholderFromLayouts(layouts);
|
||||
|
||||
expect(result.desktop).toHaveLength(2);
|
||||
expect(result.desktop).toEqual(layouts.desktop);
|
||||
expect(result.mobile).toHaveLength(2);
|
||||
expect(result.mobile).toEqual(layouts.mobile);
|
||||
});
|
||||
|
||||
it('should handle missing desktop or mobile layouts', () => {
|
||||
// Test with missing desktop property
|
||||
const layoutsWithoutDesktop = {
|
||||
mobile: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 1, h: 4 },
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 0, y: 4, w: 1, h: 4 },
|
||||
],
|
||||
} as Layouts;
|
||||
|
||||
const result1 = filterPendingPlaceholderFromLayouts(layoutsWithoutDesktop);
|
||||
|
||||
expect(result1.desktop).toBeUndefined();
|
||||
expect(result1.mobile).toHaveLength(1);
|
||||
expect(result1.mobile).toEqual([{ i: 'widget-1', x: 0, y: 0, w: 1, h: 4 }]);
|
||||
|
||||
// Test with missing mobile property
|
||||
const layoutsWithoutMobile = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 4, y: 0, w: 4, h: 4 },
|
||||
],
|
||||
} as Layouts;
|
||||
|
||||
const result2 = filterPendingPlaceholderFromLayouts(layoutsWithoutMobile);
|
||||
|
||||
expect(result2.desktop).toHaveLength(1);
|
||||
expect(result2.desktop).toEqual([
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 4 },
|
||||
]);
|
||||
expect(result2.mobile).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty layouts', () => {
|
||||
const emptyLayouts: Layouts = {
|
||||
desktop: [],
|
||||
mobile: [],
|
||||
};
|
||||
|
||||
const result = filterPendingPlaceholderFromLayouts(emptyLayouts);
|
||||
|
||||
expect(result.desktop).toEqual([]);
|
||||
expect(result.mobile).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle layouts with only pending placeholder', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 0, y: 0, w: 4, h: 4 },
|
||||
],
|
||||
mobile: [
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 0, y: 0, w: 1, h: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = filterPendingPlaceholderFromLayouts(layouts);
|
||||
|
||||
expect(result.desktop).toEqual([]);
|
||||
expect(result.mobile).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not mutate the original layouts', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 4, y: 0, w: 4, h: 4 },
|
||||
],
|
||||
mobile: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 1, h: 4 },
|
||||
{ i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY, x: 0, y: 4, w: 1, h: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
const originalDesktopLength = layouts.desktop!.length;
|
||||
const originalMobileLength = layouts.mobile!.length;
|
||||
|
||||
filterPendingPlaceholderFromLayouts(layouts);
|
||||
|
||||
expect(layouts.desktop).toHaveLength(originalDesktopLength);
|
||||
expect(layouts.mobile).toHaveLength(originalMobileLength);
|
||||
expect(
|
||||
layouts.desktop!.some(
|
||||
(item) => item.i === PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
layouts.mobile!.some(
|
||||
(item) => item.i === PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY } from '@/page-layout/constants/PendingWidgetPlaceholderLayoutKey';
|
||||
import {
|
||||
AggregateOperations,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated/graphql';
|
||||
import { prepareGridLayoutItemsWithPlaceholders } from '../prepareGridLayoutItemsWithPlaceholders';
|
||||
|
||||
describe('prepareGridLayoutItemsWithPlaceholders', () => {
|
||||
const createMockWidget = (id: string): PageLayoutWidget => ({
|
||||
id,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: `Test Widget ${id}`,
|
||||
type: WidgetType.GRAPH,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 2,
|
||||
columnSpan: 2,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'AggregateChartConfiguration',
|
||||
graphType: GraphType.AGGREGATE,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'field-id',
|
||||
displayDataLabel: false,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
describe('empty or undefined widgets', () => {
|
||||
it('should return empty placeholder when widgets is undefined', () => {
|
||||
const result = prepareGridLayoutItemsWithPlaceholders(undefined, false);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'empty-placeholder',
|
||||
type: 'placeholder',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty placeholder when widgets is an empty array', () => {
|
||||
const result = prepareGridLayoutItemsWithPlaceholders([], false);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'empty-placeholder',
|
||||
type: 'placeholder',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty placeholder even when shouldIncludePendingPlaceholder is true', () => {
|
||||
const result = prepareGridLayoutItemsWithPlaceholders([], true);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'empty-placeholder',
|
||||
type: 'placeholder',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('widgets without pending placeholder', () => {
|
||||
it('should return single widget item when given one widget', () => {
|
||||
const widget = createMockWidget('widget-1');
|
||||
const result = prepareGridLayoutItemsWithPlaceholders([widget], false);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'widget-1',
|
||||
type: 'widget',
|
||||
widget,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return multiple widget items when given multiple widgets', () => {
|
||||
const widget1 = createMockWidget('widget-1');
|
||||
const widget2 = createMockWidget('widget-2');
|
||||
const widget3 = createMockWidget('widget-3');
|
||||
const widgets = [widget1, widget2, widget3];
|
||||
|
||||
const result = prepareGridLayoutItemsWithPlaceholders(widgets, false);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'widget-1',
|
||||
type: 'widget',
|
||||
widget: widget1,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: 'widget-2',
|
||||
type: 'widget',
|
||||
widget: widget2,
|
||||
});
|
||||
expect(result[2]).toEqual({
|
||||
id: 'widget-3',
|
||||
type: 'widget',
|
||||
widget: widget3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('widgets with pending placeholder', () => {
|
||||
it('should add pending placeholder to single widget', () => {
|
||||
const widget = createMockWidget('widget-1');
|
||||
const result = prepareGridLayoutItemsWithPlaceholders([widget], true);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'widget-1',
|
||||
type: 'widget',
|
||||
widget,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
type: 'placeholder',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add pending placeholder after multiple widgets', () => {
|
||||
const widget1 = createMockWidget('widget-1');
|
||||
const widget2 = createMockWidget('widget-2');
|
||||
const widgets = [widget1, widget2];
|
||||
|
||||
const result = prepareGridLayoutItemsWithPlaceholders(widgets, true);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'widget-1',
|
||||
type: 'widget',
|
||||
widget: widget1,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: 'widget-2',
|
||||
type: 'widget',
|
||||
widget: widget2,
|
||||
});
|
||||
expect(result[2]).toEqual({
|
||||
id: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
type: 'placeholder',
|
||||
});
|
||||
});
|
||||
|
||||
it('should place pending placeholder at the end of the array', () => {
|
||||
const widgets = Array.from({ length: 5 }, (_, i) =>
|
||||
createMockWidget(`widget-${i + 1}`),
|
||||
);
|
||||
|
||||
const result = prepareGridLayoutItemsWithPlaceholders(widgets, true);
|
||||
|
||||
expect(result).toHaveLength(6);
|
||||
// Check that the last item is the pending placeholder
|
||||
const lastItem = result[result.length - 1];
|
||||
expect(lastItem).toEqual({
|
||||
id: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
type: 'placeholder',
|
||||
});
|
||||
// Check that all other items are widgets
|
||||
for (let i = 0; i < result.length - 1; i++) {
|
||||
expect(result[i].type).toBe('widget');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('type and structure verification', () => {
|
||||
it('should set correct types for widget items', () => {
|
||||
const widget = createMockWidget('widget-1');
|
||||
const result = prepareGridLayoutItemsWithPlaceholders([widget], false);
|
||||
|
||||
const item = result[0];
|
||||
expect(item.type).toBe('widget');
|
||||
expect(item).toHaveProperty('widget');
|
||||
expect(item.id).toBe(widget.id);
|
||||
if (item.type === 'widget') {
|
||||
expect(item.widget).toBe(widget);
|
||||
}
|
||||
});
|
||||
|
||||
it('should set correct types for placeholder items', () => {
|
||||
const result = prepareGridLayoutItemsWithPlaceholders(undefined, false);
|
||||
|
||||
const item = result[0];
|
||||
expect(item.type).toBe('placeholder');
|
||||
expect(item).not.toHaveProperty('widget');
|
||||
expect(item.id).toBe('empty-placeholder');
|
||||
});
|
||||
|
||||
it('should set correct types for pending placeholder', () => {
|
||||
const widget = createMockWidget('widget-1');
|
||||
const result = prepareGridLayoutItemsWithPlaceholders([widget], true);
|
||||
|
||||
const pendingPlaceholder = result[1];
|
||||
expect(pendingPlaceholder.type).toBe('placeholder');
|
||||
expect(pendingPlaceholder).not.toHaveProperty('widget');
|
||||
expect(pendingPlaceholder.id).toBe(PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY);
|
||||
});
|
||||
|
||||
it('should preserve all widget properties', () => {
|
||||
const widget = createMockWidget('widget-1');
|
||||
const result = prepareGridLayoutItemsWithPlaceholders([widget], false);
|
||||
|
||||
if (result[0].type === 'widget') {
|
||||
const resultWidget = result[0].widget;
|
||||
expect(resultWidget.id).toBe(widget.id);
|
||||
expect(resultWidget.pageLayoutTabId).toBe(widget.pageLayoutTabId);
|
||||
expect(resultWidget.title).toBe(widget.title);
|
||||
expect(resultWidget.type).toBe(widget.type);
|
||||
expect(resultWidget.objectMetadataId).toBe(widget.objectMetadataId);
|
||||
expect(resultWidget.gridPosition).toEqual(widget.gridPosition);
|
||||
expect(resultWidget.configuration).toEqual(widget.configuration);
|
||||
expect(resultWidget.createdAt).toBe(widget.createdAt);
|
||||
expect(resultWidget.updatedAt).toBe(widget.updatedAt);
|
||||
expect(resultWidget.deletedAt).toBe(widget.deletedAt);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('immutability', () => {
|
||||
it('should not mutate the input widgets array', () => {
|
||||
const widget1 = createMockWidget('widget-1');
|
||||
const widget2 = createMockWidget('widget-2');
|
||||
const originalWidgets = [widget1, widget2];
|
||||
const widgetsCopy = [...originalWidgets];
|
||||
|
||||
prepareGridLayoutItemsWithPlaceholders(originalWidgets, true);
|
||||
|
||||
// Check that the array wasn't mutated
|
||||
expect(originalWidgets).toHaveLength(2);
|
||||
expect(originalWidgets).toEqual(widgetsCopy);
|
||||
expect(originalWidgets[0]).toBe(widget1);
|
||||
expect(originalWidgets[1]).toBe(widget2);
|
||||
});
|
||||
|
||||
it('should not mutate individual widget objects', () => {
|
||||
const widget = createMockWidget('widget-1');
|
||||
const originalWidget = { ...widget };
|
||||
|
||||
prepareGridLayoutItemsWithPlaceholders([widget], true);
|
||||
|
||||
expect(widget).toEqual(originalWidget);
|
||||
});
|
||||
|
||||
it('should return a new array instance', () => {
|
||||
const widgets = [createMockWidget('widget-1')];
|
||||
const result = prepareGridLayoutItemsWithPlaceholders(widgets, false);
|
||||
|
||||
expect(result).not.toBe(widgets);
|
||||
});
|
||||
});
|
||||
});
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { updateLayoutItemConstraints } from '../updateLayoutItemConstraints';
|
||||
|
||||
describe('updateLayoutItemConstraints', () => {
|
||||
it('should update constraints for the specified layout item in desktop layout', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 2, minH: 2 },
|
||||
{ i: 'widget-2', x: 4, y: 0, w: 4, h: 3, minW: 2, minH: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-1', {
|
||||
minW: 3,
|
||||
minH: 4,
|
||||
});
|
||||
|
||||
expect(result.desktop?.[0]).toEqual({
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 3,
|
||||
minW: 3,
|
||||
minH: 4,
|
||||
});
|
||||
expect(result.desktop?.[1]).toEqual({
|
||||
i: 'widget-2',
|
||||
x: 4,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 3,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update constraints for the specified layout item in mobile layout', () => {
|
||||
const layouts: Layouts = {
|
||||
mobile: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2, minW: 1, minH: 1 },
|
||||
{ i: 'widget-2', x: 0, y: 2, w: 2, h: 2, minW: 1, minH: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-2', {
|
||||
minW: 2,
|
||||
minH: 3,
|
||||
});
|
||||
|
||||
expect(result.mobile?.[0]).toEqual({
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 2,
|
||||
h: 2,
|
||||
minW: 1,
|
||||
minH: 1,
|
||||
});
|
||||
expect(result.mobile?.[1]).toEqual({
|
||||
i: 'widget-2',
|
||||
x: 0,
|
||||
y: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
minW: 2,
|
||||
minH: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update constraints in both desktop and mobile layouts when item exists in both', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 2, minH: 2 },
|
||||
{ i: 'widget-2', x: 4, y: 0, w: 4, h: 3, minW: 2, minH: 2 },
|
||||
],
|
||||
mobile: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2, minW: 1, minH: 1 },
|
||||
{ i: 'widget-2', x: 0, y: 2, w: 2, h: 2, minW: 1, minH: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-1', {
|
||||
minW: 5,
|
||||
minH: 6,
|
||||
});
|
||||
|
||||
expect(result.desktop?.[0].minW).toBe(5);
|
||||
expect(result.desktop?.[0].minH).toBe(6);
|
||||
expect(result.mobile?.[0].minW).toBe(5);
|
||||
expect(result.mobile?.[0].minH).toBe(6);
|
||||
});
|
||||
|
||||
it('should not modify layouts when layout item does not exist', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 2, minH: 2 }],
|
||||
mobile: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2, minW: 1, minH: 1 }],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'non-existent', {
|
||||
minW: 10,
|
||||
minH: 10,
|
||||
});
|
||||
|
||||
expect(result.desktop).toEqual(layouts.desktop);
|
||||
expect(result.mobile).toEqual(layouts.mobile);
|
||||
});
|
||||
|
||||
it('should handle undefined desktop layout', () => {
|
||||
const layouts: Layouts = {
|
||||
mobile: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2, minW: 1, minH: 1 }],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-1', {
|
||||
minW: 3,
|
||||
minH: 3,
|
||||
});
|
||||
|
||||
expect(result.desktop).toBeUndefined();
|
||||
expect(result.mobile?.[0].minW).toBe(3);
|
||||
expect(result.mobile?.[0].minH).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle undefined mobile layout', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 2, minH: 2 }],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-1', {
|
||||
minW: 4,
|
||||
minH: 5,
|
||||
});
|
||||
|
||||
expect(result.desktop?.[0].minW).toBe(4);
|
||||
expect(result.desktop?.[0].minH).toBe(5);
|
||||
expect(result.mobile).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty layouts object', () => {
|
||||
const layouts: Layouts = {};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-1', {
|
||||
minW: 3,
|
||||
minH: 3,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should preserve other properties of layout items', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 3,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
static: true,
|
||||
isDraggable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-1', {
|
||||
minW: 5,
|
||||
minH: 5,
|
||||
});
|
||||
|
||||
expect(result.desktop?.[0]).toEqual({
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 3,
|
||||
minW: 5,
|
||||
minH: 5,
|
||||
static: true,
|
||||
isDraggable: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle layouts with multiple widgets and only update the target', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 2, minH: 2 },
|
||||
{ i: 'widget-2', x: 4, y: 0, w: 4, h: 3, minW: 2, minH: 2 },
|
||||
{ i: 'widget-3', x: 0, y: 3, w: 8, h: 2, minW: 3, minH: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-2', {
|
||||
minW: 6,
|
||||
minH: 4,
|
||||
});
|
||||
|
||||
expect(result.desktop?.[0].minW).toBe(2);
|
||||
expect(result.desktop?.[0].minH).toBe(2);
|
||||
expect(result.desktop?.[1].minW).toBe(6);
|
||||
expect(result.desktop?.[1].minH).toBe(4);
|
||||
expect(result.desktop?.[2].minW).toBe(3);
|
||||
expect(result.desktop?.[2].minH).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle constraints with zero values', () => {
|
||||
const layouts: Layouts = {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 2, minH: 2 }],
|
||||
};
|
||||
|
||||
const result = updateLayoutItemConstraints(layouts, 'widget-1', {
|
||||
minW: 0,
|
||||
minH: 0,
|
||||
});
|
||||
|
||||
expect(result.desktop?.[0].minW).toBe(0);
|
||||
expect(result.desktop?.[0].minH).toBe(0);
|
||||
});
|
||||
});
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { GraphType } from '~/generated/graphql';
|
||||
import { getWidgetSize } from '../getWidgetSize';
|
||||
import { updateLayoutItemConstraints } from '../updateLayoutItemConstraints';
|
||||
import { updateWidgetMinimumSizeForGraphType } from '../updateWidgetMinimumSizeForGraphType';
|
||||
|
||||
jest.mock('../getWidgetSize');
|
||||
jest.mock('../updateLayoutItemConstraints');
|
||||
|
||||
describe('updateWidgetMinimumSizeForGraphType', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should update widget minimum size for a valid graph type and existing tab', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 }],
|
||||
},
|
||||
'tab-2': {
|
||||
desktop: [{ i: 'widget-2', x: 0, y: 0, w: 4, h: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
const updatedLayouts: Layouts = {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 3, minH: 2 }],
|
||||
};
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 3, h: 2 });
|
||||
(updateLayoutItemConstraints as jest.Mock).mockReturnValue(updatedLayouts);
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.PIE,
|
||||
'widget-1',
|
||||
'tab-1',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(getWidgetSize).toHaveBeenCalledWith(GraphType.PIE, 'minimum');
|
||||
expect(updateLayoutItemConstraints).toHaveBeenCalledWith(
|
||||
mockLayouts['tab-1'],
|
||||
'widget-1',
|
||||
{ minW: 3, minH: 2 },
|
||||
);
|
||||
expect(result).toEqual({
|
||||
'tab-1': updatedLayouts,
|
||||
'tab-2': mockLayouts['tab-2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return original layouts when tab does not exist', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 3, h: 2 });
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.VERTICAL_BAR,
|
||||
'widget-2',
|
||||
'non-existent-tab',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(getWidgetSize).toHaveBeenCalledWith(
|
||||
GraphType.VERTICAL_BAR,
|
||||
'minimum',
|
||||
);
|
||||
expect(updateLayoutItemConstraints).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockLayouts);
|
||||
});
|
||||
|
||||
it('should handle different graph types correctly', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
const graphTypes = [
|
||||
GraphType.AGGREGATE,
|
||||
GraphType.GAUGE,
|
||||
GraphType.PIE,
|
||||
GraphType.VERTICAL_BAR,
|
||||
GraphType.HORIZONTAL_BAR,
|
||||
GraphType.LINE,
|
||||
];
|
||||
|
||||
graphTypes.forEach((graphType, index) => {
|
||||
const mockSize = { w: index + 2, h: index + 1 };
|
||||
(getWidgetSize as jest.Mock).mockReturnValue(mockSize);
|
||||
(updateLayoutItemConstraints as jest.Mock).mockReturnValue({
|
||||
desktop: [
|
||||
{
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 3,
|
||||
minW: mockSize.w,
|
||||
minH: mockSize.h,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
graphType,
|
||||
'widget-1',
|
||||
'tab-1',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(getWidgetSize).toHaveBeenCalledWith(graphType, 'minimum');
|
||||
expect(updateLayoutItemConstraints).toHaveBeenCalledWith(
|
||||
mockLayouts['tab-1'],
|
||||
'widget-1',
|
||||
{ minW: mockSize.w, minH: mockSize.h },
|
||||
);
|
||||
expect(result['tab-1']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve other tabs when updating a specific tab', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 }],
|
||||
},
|
||||
'tab-2': {
|
||||
desktop: [{ i: 'widget-2', x: 0, y: 0, w: 5, h: 4 }],
|
||||
},
|
||||
'tab-3': {
|
||||
desktop: [{ i: 'widget-3', x: 0, y: 0, w: 6, h: 5 }],
|
||||
},
|
||||
};
|
||||
|
||||
const updatedTab2Layouts: Layouts = {
|
||||
desktop: [{ i: 'widget-2', x: 0, y: 0, w: 5, h: 4, minW: 4, minH: 3 }],
|
||||
};
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 4, h: 3 });
|
||||
(updateLayoutItemConstraints as jest.Mock).mockReturnValue(
|
||||
updatedTab2Layouts,
|
||||
);
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.LINE,
|
||||
'widget-2',
|
||||
'tab-2',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
'tab-1': mockLayouts['tab-1'],
|
||||
'tab-2': updatedTab2Layouts,
|
||||
'tab-3': mockLayouts['tab-3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty layouts object', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {};
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 3, h: 2 });
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.PIE,
|
||||
'widget-1',
|
||||
'tab-1',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(updateLayoutItemConstraints).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle layouts with both desktop and mobile', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 }],
|
||||
mobile: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }],
|
||||
},
|
||||
};
|
||||
|
||||
const updatedLayouts: Layouts = {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3, minW: 3, minH: 2 }],
|
||||
mobile: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2, minW: 3, minH: 2 }],
|
||||
};
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 3, h: 2 });
|
||||
(updateLayoutItemConstraints as jest.Mock).mockReturnValue(updatedLayouts);
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.GAUGE,
|
||||
'widget-1',
|
||||
'tab-1',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(result['tab-1']).toEqual(updatedLayouts);
|
||||
});
|
||||
|
||||
it('should handle widget not found in tab layouts', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
const unchangedLayouts = mockLayouts['tab-1'];
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 3, h: 2 });
|
||||
(updateLayoutItemConstraints as jest.Mock).mockReturnValue(
|
||||
unchangedLayouts,
|
||||
);
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.PIE,
|
||||
'widget-non-existent',
|
||||
'tab-1',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(updateLayoutItemConstraints).toHaveBeenCalledWith(
|
||||
mockLayouts['tab-1'],
|
||||
'widget-non-existent',
|
||||
{ minW: 3, minH: 2 },
|
||||
);
|
||||
expect(result['tab-1']).toEqual(unchangedLayouts);
|
||||
});
|
||||
|
||||
it('should call updateLayoutItemConstraints with correct parameters', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 },
|
||||
{ i: 'widget-2', x: 4, y: 0, w: 4, h: 3 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 5, h: 4 });
|
||||
(updateLayoutItemConstraints as jest.Mock).mockReturnValue({
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 },
|
||||
{ i: 'widget-2', x: 4, y: 0, w: 4, h: 3, minW: 5, minH: 4 },
|
||||
],
|
||||
});
|
||||
|
||||
updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.HORIZONTAL_BAR,
|
||||
'widget-2',
|
||||
'tab-1',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(updateLayoutItemConstraints).toHaveBeenCalledWith(
|
||||
mockLayouts['tab-1'],
|
||||
'widget-2',
|
||||
{ minW: 5, minH: 4 },
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle undefined tab layouts gracefully', () => {
|
||||
const mockLayouts: Record<string, Layouts> = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 4, h: 3 }],
|
||||
},
|
||||
'tab-2': undefined as any, // Simulating an edge case
|
||||
};
|
||||
|
||||
(getWidgetSize as jest.Mock).mockReturnValue({ w: 3, h: 2 });
|
||||
|
||||
const result = updateWidgetMinimumSizeForGraphType(
|
||||
GraphType.PIE,
|
||||
'widget-1',
|
||||
'tab-2',
|
||||
mockLayouts,
|
||||
);
|
||||
|
||||
expect(updateLayoutItemConstraints).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockLayouts);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createPendingWidgetPlaceholderLayoutItem } from '@/page-layout/utils/createPendingWidgetPlaceholderLayoutItem';
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
|
||||
export const addPendingPlaceholderToLayouts = (
|
||||
baseLayouts: Layouts,
|
||||
draggedArea: { x: number; y: number; w: number; h: number },
|
||||
): Layouts => ({
|
||||
desktop: [
|
||||
...(baseLayouts.desktop ?? []),
|
||||
createPendingWidgetPlaceholderLayoutItem(draggedArea, 'desktop'),
|
||||
],
|
||||
mobile: [
|
||||
...(baseLayouts.mobile ?? []),
|
||||
createPendingWidgetPlaceholderLayoutItem(draggedArea, 'mobile'),
|
||||
],
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { DEFAULT_WIDGET_SIZE } from '@/page-layout/constants/DefaultWidgetSize';
|
||||
import { PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY } from '@/page-layout/constants/PendingWidgetPlaceholderLayoutKey';
|
||||
import { type Layout } from 'react-grid-layout';
|
||||
|
||||
export const createPendingWidgetPlaceholderLayoutItem = (
|
||||
draggedArea: { x: number; y: number; w: number; h: number },
|
||||
breakpoint: 'desktop' | 'mobile',
|
||||
): Layout => ({
|
||||
i: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
x: breakpoint === 'mobile' ? 0 : draggedArea.x,
|
||||
y: draggedArea.y,
|
||||
w:
|
||||
breakpoint === 'mobile'
|
||||
? 1
|
||||
: Math.max(draggedArea.w, DEFAULT_WIDGET_SIZE.default.w),
|
||||
h: Math.max(draggedArea.h, DEFAULT_WIDGET_SIZE.default.h),
|
||||
minW: DEFAULT_WIDGET_SIZE.minimum.w,
|
||||
minH: DEFAULT_WIDGET_SIZE.minimum.h,
|
||||
static: true,
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY } from '@/page-layout/constants/PendingWidgetPlaceholderLayoutKey';
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
|
||||
export const filterPendingPlaceholderFromLayouts = (
|
||||
layouts: Layouts,
|
||||
): Layouts => ({
|
||||
desktop: layouts.desktop?.filter(
|
||||
(layoutItem) => layoutItem.i !== PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
),
|
||||
mobile: layouts.mobile?.filter(
|
||||
(layoutItem) => layoutItem.i !== PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
),
|
||||
});
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY } from '@/page-layout/constants/PendingWidgetPlaceholderLayoutKey';
|
||||
import { type GridLayoutItem } from '@/page-layout/types/GridLayoutItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
export const prepareGridLayoutItemsWithPlaceholders = (
|
||||
widgets: PageLayoutWidget[] | undefined,
|
||||
shouldIncludePendingPlaceholder: boolean,
|
||||
): GridLayoutItem[] => {
|
||||
const hasWidgets = isDefined(widgets) && widgets.length > 0;
|
||||
|
||||
if (!hasWidgets) {
|
||||
return [
|
||||
{
|
||||
id: 'empty-placeholder',
|
||||
type: 'placeholder' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const gridLayoutItems: GridLayoutItem[] = widgets.map((widget) => ({
|
||||
id: widget.id,
|
||||
type: 'widget' as const,
|
||||
widget,
|
||||
}));
|
||||
|
||||
if (shouldIncludePendingPlaceholder) {
|
||||
gridLayoutItems.push({
|
||||
id: PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY,
|
||||
type: 'placeholder' as const,
|
||||
});
|
||||
}
|
||||
|
||||
return gridLayoutItems;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
|
||||
export const updateLayoutItemConstraints = (
|
||||
layouts: Layouts,
|
||||
layoutItemId: string,
|
||||
constraints: { minW: number; minH: number },
|
||||
): Layouts => ({
|
||||
desktop: layouts.desktop?.map((layoutItem) =>
|
||||
layoutItem.i === layoutItemId
|
||||
? {
|
||||
...layoutItem,
|
||||
minW: constraints.minW,
|
||||
minH: constraints.minH,
|
||||
}
|
||||
: layoutItem,
|
||||
),
|
||||
mobile: layouts.mobile?.map((layoutItem) =>
|
||||
layoutItem.i === layoutItemId
|
||||
? {
|
||||
...layoutItem,
|
||||
minW: constraints.minW,
|
||||
minH: constraints.minH,
|
||||
}
|
||||
: layoutItem,
|
||||
),
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { getWidgetSize } from '@/page-layout/utils/getWidgetSize';
|
||||
import { updateLayoutItemConstraints } from '@/page-layout/utils/updateLayoutItemConstraints';
|
||||
import { type GraphType } from '~/generated/graphql';
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const updateWidgetMinimumSizeForGraphType = (
|
||||
graphType: GraphType,
|
||||
widgetId: string,
|
||||
tabId: string,
|
||||
currentLayouts: Record<string, Layouts>,
|
||||
): Record<string, Layouts> => {
|
||||
const minimumSize = getWidgetSize(graphType, 'minimum');
|
||||
const currentTabLayouts = currentLayouts[tabId];
|
||||
|
||||
if (!isDefined(currentTabLayouts)) {
|
||||
return currentLayouts;
|
||||
}
|
||||
|
||||
const updatedTabLayouts = updateLayoutItemConstraints(
|
||||
currentTabLayouts,
|
||||
widgetId,
|
||||
{ minW: minimumSize.w, minH: minimumSize.h },
|
||||
);
|
||||
|
||||
return {
|
||||
...currentLayouts,
|
||||
[tabId]: updatedTabLayouts,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -62,7 +62,7 @@ export const WidgetPlaceholder = () => {
|
||||
<AnimatedPlaceholder type="noWidgets" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
<Trans>No widgets yet</Trans>
|
||||
<Trans>Add widget</Trans>
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
<Trans>Click to add your first widget</Trans>
|
||||
|
||||
Reference in New Issue
Block a user