Skip to content

Commit d131478

Browse files
nicolethoenclaude
andcommitted
feat: upgrade react-grid-layout to v2
- Update react-grid-layout from ^1.5.1 to ^2.2.2 - Migrate to v2 API: gridConfig, dragConfig, resizeConfig, dropConfig - Replace manual ResizeObserver with useContainerWidth hook - Update type imports: Layout → LayoutItem (from react-grid-layout) - Update callback signatures for readonly arrays and nullable params - Add droppingWidgetType prop for drawer→grid drag coordination - Add onWidgetDragStart/onWidgetDragEnd to WidgetDrawer - Switch tsconfig moduleResolution from "node" to "bundler" - Remove @types/react-grid-layout (types now bundled in v2) - Update @patternfly/patternfly to ^6.5.0-prerelease.33 - Fix pr-preview CI workflow: npm → yarn, corepack setup, surge deploy - Add widget configuration and layout item docs tables Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1fba4bf commit d131478

10 files changed

Lines changed: 156 additions & 122 deletions

File tree

.github/workflows/pr-preview.yml

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,36 @@ jobs:
2828
- uses: actions/setup-node@v4
2929
with:
3030
node-version: '20'
31+
- name: Enable Corepack and install correct Yarn version
32+
shell: bash
33+
run: |
34+
corepack enable
35+
corepack prepare yarn@4.10.3 --activate
3136
- uses: actions/cache@v4
32-
id: npm-cache
33-
name: Load npm deps from cache
37+
id: yarn-cache
38+
name: Load yarn deps from cache
3439
with:
35-
path: '**/node_modules'
36-
key: ${{ runner.os }}-npm-14-${{ secrets.CACHE_VERSION }}-${{ hashFiles('package-lock.json') }}
37-
- run: npm install --frozen-lockfile --legacy-peer-deps
40+
path: |
41+
node_modules
42+
**/node_modules
43+
key: ${{ runner.os }}-yarn-14-${{ secrets.CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}
44+
- run: yarn install --immutable
3845
if: steps.yarn-cache.outputs.cache-hit != 'true'
39-
- run: npm run build
46+
- run: yarn build
4047
name: Build component groups
4148
- uses: actions/cache@v4
4249
id: docs-cache
4350
name: Load webpack cache
4451
with:
4552
path: '.cache'
4653
key: ${{ runner.os }}-v4-${{ hashFiles('yarn.lock') }}
47-
- run: npm run build:docs
54+
- run: yarn build:docs
4855
name: Build docs
49-
- run: node .github/upload-preview.js packages/module/public
50-
name: Upload docs
51-
if: always()
52-
- run: npx puppeteer browsers install chrome
53-
name: Install Chrome for Puppeteer
54-
- run: npm run serve:docs & npm run test:a11y
55-
name: a11y tests
56-
- run: node .github/upload-preview.js packages/module/coverage
57-
name: Upload a11y report
58-
if: always()
56+
- name: Deploy preview to surge
57+
if: env.SURGE_LOGIN != '' && env.SURGE_TOKEN != ''
58+
run: |
59+
npx surge packages/module/public --domain pr-${{ github.event.number }}-widgetized-dashboard.surge.sh
60+
- name: Install Chrome for Puppeteer
61+
run: npx puppeteer browsers install chrome
62+
- name: a11y tests
63+
run: yarn serve:docs & yarn test:a11y

packages/module/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@patternfly/react-core": "^6.3.1",
3434
"@patternfly/react-icons": "^6.3.1",
3535
"clsx": "^2.1.0",
36-
"react-grid-layout": "^1.5.1"
36+
"react-grid-layout": "^2.2.2"
3737
},
3838
"peerDependencies": {
3939
"react": "^18 || ^19",
@@ -45,11 +45,10 @@
4545
"@babel/plugin-proposal-private-methods": "^7.18.6",
4646
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
4747
"@patternfly/documentation-framework": "^6.24.2",
48-
"@patternfly/patternfly": "^6.3.1",
48+
"@patternfly/patternfly": "^6.5.0-prerelease.33",
4949
"@patternfly/patternfly-a11y": "^5.1.0",
5050
"@patternfly/react-code-editor": "^6.3.1",
5151
"@patternfly/react-table": "^6.3.1",
52-
"@types/react-grid-layout": "^1.3.5",
5352
"monaco-editor": "^0.53.0",
5453
"nodemon": "^3.0.0",
5554
"react-monaco-editor": "^0.59.0",

packages/module/patternfly-docs/content/examples/basic.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,30 @@ const widgetMapping: WidgetMapping = {
9090
defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
9191
config: {
9292
title: 'My Widget',
93-
icon: <MyIcon />
93+
icon: <MyIcon />,
94+
headerLink: {
95+
title: 'View details',
96+
href: '/details'
97+
}
9498
},
9599
renderWidget: (id) => <MyWidgetContent />
96100
}
97101
};
98102
```
99103

104+
### Widget configuration options
105+
106+
| Property | Type | Description |
107+
|----------|------|-------------|
108+
| `defaults.w` | `number` | Default width in grid columns |
109+
| `defaults.h` | `number` | Default height in grid rows |
110+
| `defaults.maxH` | `number` | Maximum height the widget can be resized to |
111+
| `defaults.minH` | `number` | Minimum height the widget can be resized to |
112+
| `config.title` | `string` | Widget title displayed in the header |
113+
| `config.icon` | `ReactNode` | Icon displayed next to the title |
114+
| `config.headerLink` | `{ title: string, href: string }` | Optional link displayed in the widget header |
115+
| `renderWidget` | `(id: string) => ReactNode` | Function that renders the widget content |
116+
100117
## Template configuration
101118

102119
Define your initial layout using the `ExtendedTemplateConfig` type:
@@ -113,3 +130,29 @@ const initialTemplate: ExtendedTemplateConfig = {
113130
```
114131

115132
Each breakpoint (xl, lg, md, sm) should have its own layout configuration to ensure proper responsive behavior.
133+
134+
### Layout item properties
135+
136+
#### Required properties
137+
138+
| Property | Type | Description |
139+
|----------|------|-------------|
140+
| `i` | `string` | Unique identifier in format `widgetType#uuid` (e.g., `'my-widget#1'`) |
141+
| `x` | `number` | X position in grid columns (0-indexed from left) |
142+
| `y` | `number` | Y position in grid rows (0-indexed from top) |
143+
| `w` | `number` | Width in grid columns |
144+
| `h` | `number` | Height in grid rows |
145+
| `widgetType` | `string` | Must match a key in `widgetMapping` |
146+
| `title` | `string` | Display title for this widget instance |
147+
148+
#### Optional properties
149+
150+
| Property | Type | Description |
151+
|----------|------|-------------|
152+
| `minW` | `number` | Minimum width during resize |
153+
| `maxW` | `number` | Maximum width during resize |
154+
| `minH` | `number` | Minimum height during resize |
155+
| `maxH` | `number` | Maximum height during resize |
156+
| `static` | `boolean` | If `true`, widget cannot be moved or resized |
157+
| `locked` | `boolean` | If `true`, widget is locked in place |
158+
| `config` | `WidgetConfiguration` | Override the widget's default config for this instance |

packages/module/src/WidgetLayout/GridLayout.tsx

Lines changed: 59 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'react-grid-layout/css/styles.css';
2-
import ReactGridLayout, { Layout, ReactGridLayoutProps } from 'react-grid-layout';
2+
import ReactGridLayout, { useContainerWidth, LayoutItem } from 'react-grid-layout';
33
import GridTile, { SetWidgetAttribute } from './GridTile';
4-
import { useEffect, useMemo, useRef, useState } from 'react';
4+
import { useEffect, useMemo, useState } from 'react';
55
import { isWidgetType } from './utils';
66
import React from 'react';
77
import {
@@ -21,15 +21,15 @@ import { columns, breakpoints, droppingElemId, getWidgetIdentifier, extendLayout
2121
export const defaultBreakpoints = breakpoints;
2222

2323
const createSerializableConfig = (config?: WidgetConfiguration) => {
24-
if (!config) {return undefined;}
24+
if (!config) { return undefined; }
2525
return {
2626
...(config.title && { title: config.title }),
2727
...(config.headerLink && { headerLink: config.headerLink })
2828
};
2929
};
3030

31-
const getResizeHandle = (resizeHandleAxis: string, ref: React.Ref<HTMLDivElement>) => (
32-
<div ref={ref} className={`react-resizable-handle react-resizable-handle-${resizeHandleAxis}`}>
31+
const getResizeHandle = (resizeHandleAxis: string, ref: React.Ref<HTMLElement>) => (
32+
<div ref={ref as React.Ref<HTMLDivElement>} className={`react-resizable-handle react-resizable-handle-${resizeHandleAxis}`}>
3333
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
3434
<path d="M16 1.14286L14.8571 0L0 14.8571V16H1.14286L16 1.14286Z" fill="currentColor" />
3535
</svg>
@@ -57,6 +57,8 @@ export interface GridLayoutProps {
5757
onDrawerExpandChange?: (expanded: boolean) => void;
5858
/** Currently active widgets (for tracking) */
5959
onActiveWidgetsChange?: (widgetTypes: string[]) => void;
60+
/** Widget type currently being dragged from drawer */
61+
droppingWidgetType?: string;
6062
}
6163

6264
const LayoutEmptyState = ({
@@ -100,35 +102,35 @@ const GridLayout = ({
100102
showEmptyState = true,
101103
onDrawerExpandChange,
102104
onActiveWidgetsChange,
105+
droppingWidgetType,
103106
}: GridLayoutProps) => {
104107
const [isDragging, setIsDragging] = useState(false);
105108
const [isInitialRender, setIsInitialRender] = useState(true);
106109
const [layoutVariant, setLayoutVariant] = useState<Variants>('xl');
107-
const [layoutWidth, setLayoutWidth] = useState<number>(1200);
108-
const layoutRef = useRef<HTMLDivElement>(null);
109110

110-
const [currentDropInItem, setCurrentDropInItem] = useState<string | undefined>();
111+
// Use v2 hook for container width measurement
112+
const { width: layoutWidth, containerRef, mounted } = useContainerWidth();
113+
111114
const [internalTemplate, setInternalTemplate] = useState<ExtendedTemplateConfig>(template);
112115

113116
// Sync external template changes to internal state
114117
useEffect(() => {
115118
setInternalTemplate(template);
116119
}, [template]);
117120

118-
const droppingItemTemplate: ReactGridLayoutProps['droppingItem'] = useMemo(() => {
119-
if (currentDropInItem && isWidgetType(widgetMapping, currentDropInItem)) {
120-
const widget = widgetMapping[currentDropInItem];
121+
const droppingItemTemplate = useMemo(() => {
122+
if (droppingWidgetType && isWidgetType(widgetMapping, droppingWidgetType)) {
123+
const widget = widgetMapping[droppingWidgetType];
121124
if (!widget) {return undefined;}
122125
return {
123126
...widget.defaults,
124127
i: droppingElemId,
125-
widgetType: currentDropInItem,
126-
title: 'New title',
127-
config: createSerializableConfig(widget.config)
128+
x: 0,
129+
y: 0,
128130
};
129131
}
130132
return undefined;
131-
}, [currentDropInItem, widgetMapping]);
133+
}, [droppingWidgetType, widgetMapping]);
132134

133135
const setWidgetAttribute: SetWidgetAttribute = (id, attributeName, value) => {
134136
const newTemplate = Object.entries(internalTemplate).reduce(
@@ -154,10 +156,11 @@ const GridLayout = ({
154156
onTemplateChange?.(newTemplate);
155157
};
156158

157-
const onDrop: ReactGridLayoutProps['onDrop'] = (_layout: ExtendedLayoutItem[], layoutItem: ExtendedLayoutItem, event: DragEvent) => {
158-
const data = event.dataTransfer?.getData('text') || '';
159+
const onDrop = (_layout: readonly LayoutItem[], layoutItem: LayoutItem | undefined, event: Event) => {
160+
if (!layoutItem) { return; }
161+
const dragEvent = event as DragEvent;
162+
const data = dragEvent.dataTransfer?.getData('text') || '';
159163
if (isWidgetType(widgetMapping, data)) {
160-
setCurrentDropInItem(undefined);
161164
const widget = widgetMapping[data];
162165
if (!widget) {return;}
163166
const newTemplate = Object.entries(internalTemplate).reduce((acc, [size, layout]) => {
@@ -188,81 +191,75 @@ const GridLayout = ({
188191
),
189192
};
190193
}, {} as ExtendedTemplateConfig);
191-
194+
192195
setInternalTemplate(newTemplate);
193196
onTemplateChange?.(newTemplate);
194197
analytics?.('widget-layout.widget-add', { data });
195198
}
196-
event.preventDefault();
199+
dragEvent.preventDefault();
197200
};
198201

199-
const onLayoutChange = (currentLayout: Layout[]) => {
202+
const onLayoutChange = (currentLayout: readonly LayoutItem[]) => {
200203
if (isInitialRender) {
201204
setIsInitialRender(false);
202205
const activeWidgets = activeLayout.map((item) => item.widgetType);
203206
onActiveWidgetsChange?.(activeWidgets);
204207
return;
205208
}
206-
if (isLayoutLocked || currentDropInItem) {
209+
if (isLayoutLocked || droppingWidgetType) {
207210
return;
208211
}
209212

210-
const newTemplate = extendLayout({ ...internalTemplate, [layoutVariant]: currentLayout });
213+
// Create mutable copy of readonly layout for extendLayout
214+
const newTemplate = extendLayout({ ...internalTemplate, [layoutVariant]: [...currentLayout] });
211215
const activeWidgets = activeLayout.map((item) => item.widgetType);
212216
onActiveWidgetsChange?.(activeWidgets);
213-
217+
214218
setInternalTemplate(newTemplate);
215219
onTemplateChange?.(newTemplate);
216220
};
217221

222+
// Update layout variant when container width changes
218223
useEffect(() => {
219-
const currentWidth = layoutRef.current?.getBoundingClientRect().width ?? 1200;
220-
const variant: Variants = getGridDimensions(currentWidth);
221-
setLayoutVariant(variant);
222-
setLayoutWidth(currentWidth);
223-
224-
const observer = new ResizeObserver((entries) => {
225-
if (!entries[0]) {return;}
226-
227-
const currentWidth = entries[0].contentRect.width;
228-
const variant: Variants = getGridDimensions(currentWidth);
224+
if (mounted && layoutWidth > 0) {
225+
const variant: Variants = getGridDimensions(layoutWidth);
229226
setLayoutVariant(variant);
230-
setLayoutWidth(currentWidth);
231-
});
232-
233-
if (layoutRef.current) {
234-
observer.observe(layoutRef.current);
235227
}
236-
237-
return () => {
238-
observer.disconnect();
239-
};
240-
}, []);
228+
}, [layoutWidth, mounted]);
241229

242230
const activeLayout = internalTemplate[layoutVariant] || [];
243-
231+
232+
// Use default width before mount, actual width after
233+
const effectiveWidth = mounted && layoutWidth > 0 ? layoutWidth : 1200;
234+
244235
return (
245-
<div className="pf-v6-widget-layout-container" ref={layoutRef}>
246-
{activeLayout.length === 0 && !currentDropInItem && showEmptyState && (
236+
<div className="pf-v6-widget-layout-container" ref={containerRef}>
237+
{activeLayout.length === 0 && !droppingWidgetType && showEmptyState && (
247238
emptyStateComponent || <LayoutEmptyState onDrawerExpandChange={onDrawerExpandChange} documentationLink={documentationLink} />
248239
)}
249-
<ReactGridLayout
240+
{mounted && <ReactGridLayout
250241
key={'grid-' + layoutVariant}
251-
draggableHandle=".pf-v6-widget-drag-handle"
252242
layout={internalTemplate[layoutVariant]}
253-
cols={columns[layoutVariant]}
254-
rowHeight={56}
255-
width={layoutWidth}
256-
isDraggable={!isLayoutLocked}
257-
isResizable={!isLayoutLocked}
258-
resizeHandle={getResizeHandle as unknown as ReactGridLayoutProps['resizeHandle']}
259-
resizeHandles={['sw', 'nw', 'se', 'ne']}
243+
width={effectiveWidth}
260244
droppingItem={droppingItemTemplate}
261-
isDroppable={!isLayoutLocked}
245+
gridConfig={{
246+
cols: columns[layoutVariant],
247+
rowHeight: 56,
248+
}}
249+
dragConfig={{
250+
handle: '.pf-v6-widget-drag-handle',
251+
enabled: !isLayoutLocked,
252+
}}
253+
resizeConfig={{
254+
enabled: !isLayoutLocked,
255+
handles: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'],
256+
handleComponent: getResizeHandle,
257+
}}
258+
dropConfig={{
259+
enabled: !isLayoutLocked,
260+
}}
262261
onDrop={onDrop}
263-
onDragStart={() => setCurrentDropInItem(undefined)}
264-
useCSSTransforms
265-
verticalCompact
262+
onDragStart={() => {}}
266263
onLayoutChange={onLayoutChange}
267264
>
268265
{activeLayout
@@ -281,7 +278,7 @@ const GridLayout = ({
281278
widgetType={widgetType}
282279
widgetConfig={{
283280
...layoutItem,
284-
colWidth: layoutWidth / columns[layoutVariant],
281+
colWidth: effectiveWidth / columns[layoutVariant],
285282
config,
286283
maxH: layoutItem.maxH ?? widget.defaults.maxH,
287284
minH: layoutItem.minH ?? widget.defaults.minH,
@@ -296,10 +293,9 @@ const GridLayout = ({
296293
);
297294
})
298295
.filter((layoutItem) => layoutItem !== null)}
299-
</ReactGridLayout>
296+
</ReactGridLayout>}
300297
</div>
301298
);
302299
};
303300

304301
export default GridLayout;
305-

packages/module/src/WidgetLayout/GridTile.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import MinusCircleIcon from '@patternfly/react-icons/dist/esm/icons/minus-circle
2626
import UnlockIcon from '@patternfly/react-icons/dist/esm/icons/unlock-icon';
2727
import React, { useMemo, useState } from 'react';
2828
import clsx from 'clsx';
29-
import { Layout } from 'react-grid-layout';
29+
import type { LayoutItem } from 'react-grid-layout';
3030
import { ExtendedLayoutItem, WidgetConfiguration, AnalyticsTracker } from './types';
3131

3232
export type SetWidgetAttribute = <T extends string | number | boolean>(id: string, attributeName: keyof ExtendedLayoutItem, value: T) => void;
@@ -37,7 +37,7 @@ export type GridTileProps = React.PropsWithChildren<{
3737
setIsDragging: (isDragging: boolean) => void;
3838
isDragging: boolean;
3939
setWidgetAttribute: SetWidgetAttribute;
40-
widgetConfig: Layout & {
40+
widgetConfig: LayoutItem & {
4141
colWidth: number;
4242
locked?: boolean;
4343
config?: WidgetConfiguration;

0 commit comments

Comments
 (0)