[DASHBOARDS] Add the ability to click to create a widget (#16673)

## Video QA


https://github.com/user-attachments/assets/813a2040-6ee9-418c-b7da-1126c9720446
This commit is contained in:
Raphaël Bosi
2025-12-18 15:37:22 +01:00
committed by GitHub
parent 4bdd866a20
commit 01ffca0cef
10 changed files with 119 additions and 2 deletions
@@ -7,6 +7,7 @@ import {
PAGE_LAYOUT_CONFIG,
type PageLayoutBreakpoint,
} from '@/page-layout/constants/PageLayoutBreakpoints';
import { PAGE_LAYOUT_GRID_ITEM_Z_INDEX } from '@/page-layout/constants/PageLayoutGridItemZIndex';
import { PAGE_LAYOUT_GRID_MARGIN } from '@/page-layout/constants/PageLayoutGridMargin';
import { PAGE_LAYOUT_GRID_ROW_HEIGHT } from '@/page-layout/constants/PageLayoutGridRowHeight';
import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange';
@@ -54,6 +55,10 @@ const StyledGridContainer = styled.div`
user-select: auto;
}
.react-grid-item {
z-index: ${PAGE_LAYOUT_GRID_ITEM_Z_INDEX};
}
.react-grid-item:hover .widget-card-resize-handle {
display: block !important;
}
@@ -1,4 +1,6 @@
import { type PageLayoutBreakpoint } from '@/page-layout/constants/PageLayoutBreakpoints';
import { PAGE_LAYOUT_GRID_OVERLAY_Z_INDEX } from '@/page-layout/constants/PageLayoutGridOverlayZIndex';
import { useCreateWidgetFromClick } from '@/page-layout/hooks/useCreateWidgetFromClick';
import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/pageLayoutCurrentBreakpointComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
@@ -26,7 +28,7 @@ const StyledGridOverlay = styled.div<{
gap: ${({ theme }) => theme.spacing(2)};
pointer-events: ${({ isDragSelecting }) =>
isDragSelecting ? 'auto' : 'none'};
z-index: 0;
z-index: ${PAGE_LAYOUT_GRID_OVERLAY_Z_INDEX};
`;
const StyledGridCell = styled.div<{ isSelected?: boolean }>`
@@ -36,6 +38,7 @@ const StyledGridCell = styled.div<{ isSelected?: boolean }>`
${({ theme, isSelected }) =>
isSelected ? theme.color.blue7 : theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
@@ -59,6 +62,8 @@ export const PageLayoutGridOverlay = () => {
const activeTabId = useRecoilComponentValue(activeTabIdComponentState);
const { createWidgetFromClick } = useCreateWidgetFromClick();
const numberOfRows = useMemo(() => {
const currentTabLayouts = pageLayoutCurrentLayouts[activeTabId ?? ''] || {
desktop: [],
@@ -92,6 +97,7 @@ export const PageLayoutGridOverlay = () => {
key={i}
data-selectable-id={cellId}
isSelected={pageLayoutSelectedCells.has(cellId)}
onClick={() => createWidgetFromClick(cellId)}
/>
);
},
@@ -0,0 +1 @@
export const PAGE_LAYOUT_GRID_ITEM_Z_INDEX = 2;
@@ -0,0 +1 @@
export const PAGE_LAYOUT_GRID_OVERLAY_Z_INDEX = 1;
@@ -8,6 +8,6 @@ export const WIDGET_SIZES: Partial<Record<WidgetType, WidgetSizeConfig>> = {
},
[WidgetType.STANDALONE_RICH_TEXT]: {
default: { w: 4, h: 4 },
minimum: { w: 1, h: 1 },
minimum: { w: 2, h: 2 },
},
};
@@ -0,0 +1,59 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { pageLayoutDraggedAreaComponentState } from '../../states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '../../states/pageLayoutEditingWidgetIdComponentState';
import { useCreateWidgetFromClick } from '../useCreateWidgetFromClick';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
} from './PageLayoutTestWrapper';
jest.mock(
'@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu',
);
describe('useCreateWidgetFromClick', () => {
const mockNavigatePageLayoutCommandMenu = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useNavigatePageLayoutCommandMenu as jest.Mock).mockReturnValue({
navigatePageLayoutCommandMenu: mockNavigatePageLayoutCommandMenu,
});
});
it('should set dragged area and navigate to widget selection when called with a cellId', () => {
const { result } = renderHook(
() => ({
createWidget: useCreateWidgetFromClick(),
draggedArea: useRecoilComponentValue(
pageLayoutDraggedAreaComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
editingWidgetId: useRecoilComponentValue(
pageLayoutEditingWidgetIdComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper>{children}</PageLayoutTestWrapper>
),
},
);
act(() => {
result.current.createWidget.createWidgetFromClick('cell-2-3');
});
expect(result.current.draggedArea).toEqual({ x: 2, y: 3, w: 1, h: 1 });
expect(result.current.editingWidgetId).toBeNull();
expect(mockNavigatePageLayoutCommandMenu).toHaveBeenCalledWith({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
});
});
@@ -102,6 +102,7 @@ describe('useEndPageLayoutDragSelection', () => {
expect(mockNavigatePageLayoutCommandMenu).toHaveBeenCalledWith({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
});
@@ -0,0 +1,42 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { parseCellIdToCoordinates } from '@/page-layout/utils/parseCellIdToCoordinates';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
export const useCreateWidgetFromClick = () => {
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
pageLayoutDraggedAreaComponentState,
);
const pageLayoutEditingWidgetIdState = useRecoilComponentCallbackState(
pageLayoutEditingWidgetIdComponentState,
);
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const createWidgetFromClick = useRecoilCallback(
({ set }) =>
(cellId: string) => {
const { col, row } = parseCellIdToCoordinates(cellId);
const bounds = { x: col, y: row, w: 1, h: 1 };
set(pageLayoutDraggedAreaState, bounds);
set(pageLayoutEditingWidgetIdState, null);
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
},
[
navigatePageLayoutCommandMenu,
pageLayoutDraggedAreaState,
pageLayoutEditingWidgetIdState,
],
);
return { createWidgetFromClick };
};
@@ -52,6 +52,7 @@ export const useEndPageLayoutDragSelection = (
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
}
}
@@ -38,6 +38,7 @@ export const DashboardWidgetPlaceholder = () => {
}
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
};