Skip to content

Commit dc2cf02

Browse files
committed
update rgl to v2
1 parent 47f44f6 commit dc2cf02

File tree

9 files changed

+142
-86
lines changed

9 files changed

+142
-86
lines changed

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",
@@ -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
@@ -63,13 +63,30 @@ const widgetMapping: WidgetMapping = {
6363
defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
6464
config: {
6565
title: 'My Widget',
66-
icon: <MyIcon />
66+
icon: <MyIcon />,
67+
headerLink: {
68+
title: 'View details',
69+
href: '/details'
70+
}
6771
},
6872
renderWidget: (id) => <MyWidgetContent />
6973
}
7074
};
7175
```
7276

77+
### Widget configuration options
78+
79+
| Property | Type | Description |
80+
|----------|------|-------------|
81+
| `defaults.w` | `number` | Default width in grid columns |
82+
| `defaults.h` | `number` | Default height in grid rows |
83+
| `defaults.maxH` | `number` | Maximum height the widget can be resized to |
84+
| `defaults.minH` | `number` | Minimum height the widget can be resized to |
85+
| `config.title` | `string` | Widget title displayed in the header |
86+
| `config.icon` | `ReactNode` | Icon displayed next to the title |
87+
| `config.headerLink` | `{ title: string, href: string }` | Optional link displayed in the widget header |
88+
| `renderWidget` | `(id: string) => ReactNode` | Function that renders the widget content |
89+
7390
## Template configuration
7491

7592
Define your initial layout using the `ExtendedTemplateConfig` type:
@@ -86,3 +103,29 @@ const initialTemplate: ExtendedTemplateConfig = {
86103
```
87104

88105
Each breakpoint (xl, lg, md, sm) should have its own layout configuration to ensure proper responsive behavior.
106+
107+
### Layout item properties
108+
109+
#### Required properties
110+
111+
| Property | Type | Description |
112+
|----------|------|-------------|
113+
| `i` | `string` | Unique identifier in format `widgetType#uuid` (e.g., `'my-widget#1'`) |
114+
| `x` | `number` | X position in grid columns (0-indexed from left) |
115+
| `y` | `number` | Y position in grid rows (0-indexed from top) |
116+
| `w` | `number` | Width in grid columns |
117+
| `h` | `number` | Height in grid rows |
118+
| `widgetType` | `string` | Must match a key in `widgetMapping` |
119+
| `title` | `string` | Display title for this widget instance |
120+
121+
#### Optional properties
122+
123+
| Property | Type | Description |
124+
|----------|------|-------------|
125+
| `minW` | `number` | Minimum width during resize |
126+
| `maxW` | `number` | Maximum width during resize |
127+
| `minH` | `number` | Minimum height during resize |
128+
| `maxH` | `number` | Maximum height during resize |
129+
| `static` | `boolean` | If `true`, widget cannot be moved or resized |
130+
| `locked` | `boolean` | If `true`, widget is locked in place |
131+
| `config` | `WidgetConfiguration` | Override the widget's default config for this instance |

packages/module/src/WidgetLayout/GridLayout.tsx

Lines changed: 55 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import 'react-grid-layout/css/styles.css';
22
import './styles';
3-
import ReactGridLayout, { Layout, ReactGridLayoutProps } from 'react-grid-layout';
3+
import ReactGridLayout, { useContainerWidth, LayoutItem } from 'react-grid-layout';
44
import GridTile, { SetWidgetAttribute } from './GridTile';
5-
import { useEffect, useMemo, useRef, useState } from 'react';
5+
import { useEffect, useMemo, useState } from 'react';
66
import { isWidgetType } from './utils';
77
import React from 'react';
88
import {
@@ -30,8 +30,8 @@ const createSerializableConfig = (config?: WidgetConfiguration) => {
3030
// SVG resize handle as inline data URI
3131
const resizeHandleSvg = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE2IDEuMTQyODZMMTQuODU3MSAwTDAgMTQuODU3MVYxNkgxLjE0Mjg2TDE2IDEuMTQyODZaIiBmaWxsPSIjRDJEMkQyIi8+Cjwvc3ZnPgo=';
3232

33-
const getResizeHandle = (resizeHandleAxis: string, ref: React.Ref<HTMLDivElement>) => (
34-
<div ref={ref} className={`react-resizable-handle react-resizable-handle-${resizeHandleAxis}`}>
33+
const getResizeHandle = (resizeHandleAxis: string, ref: React.Ref<HTMLElement>) => (
34+
<div ref={ref as React.Ref<HTMLDivElement>} className={`react-resizable-handle react-resizable-handle-${resizeHandleAxis}`}>
3535
<img src={resizeHandleSvg} alt="Resize handle" />
3636
</div>
3737
);
@@ -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);
110+
111+
// Use v2 hook for container width measurement
112+
const { width: layoutWidth, containerRef, mounted } = useContainerWidth();
109113

110-
const [currentDropInItem, setCurrentDropInItem] = useState<string | undefined>();
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]) => {
@@ -193,76 +196,70 @@ const GridLayout = ({
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);
213217

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] || [];
243231

232+
// Use default width before mount, actual width after
233+
const effectiveWidth = mounted && layoutWidth > 0 ? layoutWidth : 1200;
234+
244235
return (
245-
<div id="widget-layout-container" style={{ position: 'relative' }} ref={layoutRef}>
246-
{activeLayout.length === 0 && !currentDropInItem && showEmptyState && (
236+
<div id="widget-layout-container" style={{ position: 'relative' }} 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=".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: '.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
@@ -279,7 +276,7 @@ const GridLayout = ({
279276
isDragging={isDragging}
280277
setIsDragging={setIsDragging}
281278
widgetType={widgetType}
282-
widgetConfig={{ ...layoutItem, colWidth: layoutWidth / columns[layoutVariant], config }}
279+
widgetConfig={{ ...layoutItem, colWidth: effectiveWidth / columns[layoutVariant], config }}
283280
setWidgetAttribute={setWidgetAttribute}
284281
removeWidget={removeWidget}
285282
analytics={analytics}
@@ -290,7 +287,7 @@ const GridLayout = ({
290287
);
291288
})
292289
.filter((layoutItem) => layoutItem !== null)}
293-
</ReactGridLayout>
290+
</ReactGridLayout>}
294291
</div>
295292
);
296293
};

packages/module/src/WidgetLayout/GridTile.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { CompressIcon, EllipsisVIcon, ExpandIcon, GripVerticalIcon, LockIcon, MinusCircleIcon, UnlockIcon } from '@patternfly/react-icons';
2222
import React, { useMemo, useState } from 'react';
2323
import clsx from 'clsx';
24-
import { Layout } from 'react-grid-layout';
24+
import type { LayoutItem } from 'react-grid-layout';
2525
import { ExtendedLayoutItem, WidgetConfiguration, AnalyticsTracker } from './types';
2626

2727
export type SetWidgetAttribute = <T extends string | number | boolean>(id: string, attributeName: keyof ExtendedLayoutItem, value: T) => void;
@@ -32,7 +32,7 @@ export type GridTileProps = React.PropsWithChildren<{
3232
setIsDragging: (isDragging: boolean) => void;
3333
isDragging: boolean;
3434
setWidgetAttribute: SetWidgetAttribute;
35-
widgetConfig: Layout & {
35+
widgetConfig: LayoutItem & {
3636
colWidth: number;
3737
locked?: boolean;
3838
config?: WidgetConfiguration;

packages/module/src/WidgetLayout/WidgetDrawer.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export type WidgetDrawerProps = React.PropsWithChildren<{
2828
onOpenChange?: (isOpen: boolean) => void;
2929
/** Custom instruction text */
3030
instructionText?: string;
31+
/** Callback when widget drag starts from drawer */
32+
onWidgetDragStart?: (widgetType: string) => void;
33+
/** Callback when widget drag ends */
34+
onWidgetDragEnd?: () => void;
3135
}>;
3236

3337
const WidgetWrapper = ({ widgetType, config, onDragStart, onDragEnd }: {
@@ -91,6 +95,8 @@ const WidgetDrawer = ({
9195
isOpen: controlledIsOpen,
9296
onOpenChange,
9397
instructionText,
98+
onWidgetDragStart,
99+
onWidgetDragEnd,
94100
}: WidgetDrawerProps) => {
95101
const [internalIsOpen, setInternalIsOpen] = useState(false);
96102

@@ -142,8 +148,8 @@ const WidgetDrawer = ({
142148
<WidgetWrapper
143149
widgetType={type}
144150
config={widget.config}
145-
onDragStart={() => {}}
146-
onDragEnd={() => {}}
151+
onDragStart={(widgetType) => onWidgetDragStart?.(widgetType)}
152+
onDragEnd={() => onWidgetDragEnd?.()}
147153
/>
148154
</GalleryItem>
149155
))}

packages/module/src/WidgetLayout/WidgetLayout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const WidgetLayout = ({
4444
const [template, setTemplate] = useState<ExtendedTemplateConfig>(initialTemplate);
4545
const [drawerOpen, setDrawerOpen] = useState(initialDrawerOpen);
4646
const [currentlyUsedWidgets, setCurrentlyUsedWidgets] = useState<string[]>([]);
47+
const [droppingWidgetType, setDroppingWidgetType] = useState<string | undefined>();
4748

4849
const handleTemplateChange = (newTemplate: ExtendedTemplateConfig) => {
4950
setTemplate(newTemplate);
@@ -70,6 +71,7 @@ const WidgetLayout = ({
7071
showEmptyState={showEmptyState}
7172
onDrawerExpandChange={handleDrawerExpandChange}
7273
onActiveWidgetsChange={handleActiveWidgetsChange}
74+
droppingWidgetType={droppingWidgetType}
7375
/>
7476
);
7577

@@ -84,6 +86,8 @@ const WidgetLayout = ({
8486
isOpen={drawerOpen}
8587
onOpenChange={setDrawerOpen}
8688
instructionText={drawerInstructionText}
89+
onWidgetDragStart={setDroppingWidgetType}
90+
onWidgetDragEnd={() => setDroppingWidgetType(undefined)}
8791
>
8892
{gridLayout}
8993
</WidgetDrawer>

packages/module/src/WidgetLayout/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Layout } from 'react-grid-layout';
1+
import type { LayoutItem } from 'react-grid-layout';
22

33
export const widgetIdSeparator = '#';
44

55
export type Variants = 'sm' | 'md' | 'lg' | 'xl';
66

7-
export type LayoutWithTitle = Layout & { title: string };
7+
export type LayoutWithTitle = LayoutItem & { title: string };
88

99
export type TemplateConfig = {
1010
[k in Variants]: LayoutWithTitle[];

packages/module/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
4646

4747
/* Module Resolution Options */
48-
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
48+
"moduleResolution": "bundler" /* Updated to 'bundler' for react-grid-layout v2 ES module subpath exports */,
4949
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
5050
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
5151
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */

0 commit comments

Comments
 (0)