Files
stack/apps/web/src/components/widgets/WidgetGrid.tsx
Jason Woltje ac1f2c176f
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: Resolve all ESLint errors and warnings in web package
Fixes all 542 ESLint problems in the web package to achieve 0 errors and 0 warnings.

Changes:
- Fixed 144 issues: nullish coalescing, return types, unused variables
- Fixed 118 issues: unnecessary conditions, type safety, template literals
- Fixed 79 issues: non-null assertions, unsafe assignments, empty functions
- Fixed 67 issues: explicit return types, promise handling, enum comparisons
- Fixed 45 final warnings: missing return types, optional chains
- Fixed 25 typecheck-related issues: async/await, type assertions, formatting
- Fixed JSX.Element namespace errors across 90+ files

All Quality Rails violations resolved. Lint and typecheck both pass with 0 problems.

Files modified: 118 components, tests, hooks, and utilities

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 00:10:03 -06:00

166 lines
5.0 KiB
TypeScript

/**
* WidgetGrid - Draggable grid layout for widgets
* Uses react-grid-layout for drag-and-drop functionality
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { useCallback, useMemo } from "react";
import GridLayout from "react-grid-layout";
import type { Layout, LayoutItem } from "react-grid-layout";
import type { WidgetPlacement } from "@mosaic/shared";
import { getWidgetByName } from "./WidgetRegistry";
import { BaseWidget } from "./BaseWidget";
import "react-grid-layout/css/styles.css";
// Simple classnames utility
function cn(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(" ");
}
export interface WidgetGridProps {
layout: WidgetPlacement[];
onLayoutChange: (layout: WidgetPlacement[]) => void;
onRemoveWidget?: (widgetId: string) => void;
isEditing?: boolean;
className?: string;
}
export function WidgetGrid({
layout,
onLayoutChange,
onRemoveWidget,
isEditing = false,
className,
}: WidgetGridProps): React.JSX.Element {
// Convert WidgetPlacement to react-grid-layout Layout format
const gridLayout: Layout = useMemo(
() =>
layout.map((item): LayoutItem => {
const layoutItem: LayoutItem = {
i: item.i,
x: item.x,
y: item.y,
w: item.w,
h: item.h,
static: !isEditing || (item.static ?? false),
isDraggable: isEditing && item.isDraggable !== false,
isResizable: isEditing && item.isResizable !== false,
};
if (item.minW !== undefined) layoutItem.minW = item.minW;
if (item.maxW !== undefined) layoutItem.maxW = item.maxW;
if (item.minH !== undefined) layoutItem.minH = item.minH;
if (item.maxH !== undefined) layoutItem.maxH = item.maxH;
return layoutItem;
}),
[layout, isEditing]
);
const handleLayoutChange = useCallback(
(newLayout: Layout) => {
const updatedLayout: WidgetPlacement[] = newLayout.map((item): WidgetPlacement => {
const placement: WidgetPlacement = {
i: item.i,
x: item.x,
y: item.y,
w: item.w,
h: item.h,
};
if (item.minW !== undefined) placement.minW = item.minW;
if (item.maxW !== undefined) placement.maxW = item.maxW;
if (item.minH !== undefined) placement.minH = item.minH;
if (item.maxH !== undefined) placement.maxH = item.maxH;
if (item.static !== undefined) placement.static = item.static;
if (item.isDraggable !== undefined) placement.isDraggable = item.isDraggable;
if (item.isResizable !== undefined) placement.isResizable = item.isResizable;
return placement;
});
onLayoutChange(updatedLayout);
},
[onLayoutChange]
);
const handleRemoveWidget = useCallback(
(widgetId: string) => {
if (onRemoveWidget) {
onRemoveWidget(widgetId);
}
},
[onRemoveWidget]
);
// Empty state
if (layout.length === 0) {
return (
<div className="flex items-center justify-center h-full min-h-[400px] bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<div className="text-center">
<p className="text-gray-500 text-lg font-medium">No widgets yet</p>
<p className="text-gray-400 text-sm mt-1">Add widgets to customize your dashboard</p>
</div>
</div>
);
}
return (
<div className={cn("widget-grid-container", className)}>
<GridLayout
className="layout"
layout={gridLayout}
onLayoutChange={handleLayoutChange}
width={1200}
gridConfig={{
cols: 12,
rowHeight: 100,
}}
dragConfig={{
enabled: isEditing,
}}
resizeConfig={{
enabled: isEditing,
}}
data-testid="grid-layout"
>
{layout.map((item) => {
// Extract widget type from widget ID (format: "WidgetType-uuid")
const widgetType = item.i.split("-")[0]!;
const widgetDef = getWidgetByName(widgetType);
if (!widgetDef) {
return (
<div key={item.i} data-testid={`widget-${item.i}`}>
<BaseWidget id={item.i} title="Unknown Widget" error="Widget not found">
<div />
</BaseWidget>
</div>
);
}
const WidgetComponent = widgetDef.component;
return (
<div key={item.i} data-testid={`widget-${item.i}`}>
<BaseWidget
id={item.i}
title={widgetDef.displayName}
description={widgetDef.description}
{...(isEditing &&
onRemoveWidget && {
onRemove: (): void => {
handleRemoveWidget(item.i);
},
})}
>
<WidgetComponent id={item.i} />
</BaseWidget>
</div>
);
})}
</GridLayout>
</div>
);
}