feature/IO-3545-Production-Board-List-DND - Checkpoint

This commit is contained in:
Dave
2026-02-03 14:56:04 -05:00
parent 2cc9fa961e
commit da0462f14c
2 changed files with 119 additions and 45 deletions

View File

@@ -3,7 +3,7 @@ import { PageHeader } from "@ant-design/pro-layout";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd"; import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { closestCenter, DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { closestCenter, DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable"; import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
@@ -84,7 +84,14 @@ function DraggableHeaderCell(props) {
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser, isDarkMode }) { export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser, isDarkMode }) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// NEW: smoother resize
const [isResizing, setIsResizing] = useState(false);
const resizeRafRef = useRef(null);
const pendingResizeRef = useRef(null);
const [activeId, setActiveId] = useState(null); const [activeId, setActiveId] = useState(null);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { useSensor(PointerSensor, {
activationConstraint: { activationConstraint: {
@@ -92,6 +99,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
} }
}) })
); );
const { const {
treatments: { Production_List_Status_Colors, Enhanced_Payroll } treatments: { Production_List_Status_Colors, Enhanced_Payroll }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -99,8 +107,10 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
names: ["Production_List_Status_Colors", "Enhanced_Payroll"], names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
splitKey: bodyshop.imexshopid splitKey: bodyshop.imexshopid
}); });
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email); const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
const defaultView = assoc?.default_prod_list_view; const defaultView = assoc?.default_prod_list_view;
const initialStateRef = useRef( const initialStateRef = useRef(
(bodyshop.production_config && (bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) || bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
@@ -109,6 +119,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
filteredInfo: { text: "" } filteredInfo: { text: "" }
} }
); );
const initialColumnsRef = useRef( const initialColumnsRef = useRef(
(initialStateRef.current && (initialStateRef.current &&
bodyshop?.production_config bodyshop?.production_config
@@ -129,14 +140,30 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
})) || })) ||
[] []
); );
const [state, setState] = useState(initialStateRef.current); const [state, setState] = useState(initialStateRef.current);
const [columns, setColumns] = useState(initialColumnsRef.current); const [columns, setColumns] = useState(initialColumnsRef.current);
const { t } = useTranslation(); const { t } = useTranslation();
const matchingColumnConfig = useMemo(() => { const matchingColumnConfig = useMemo(() => {
return bodyshop?.production_config?.find((p) => p.name === defaultView); return bodyshop?.production_config?.find((p) => p.name === defaultView);
}, [bodyshop.production_config, defaultView]); }, [bodyshop.production_config, defaultView]);
// NEW: cleanup RAF on unmount
useEffect(() => { useEffect(() => {
return () => {
if (resizeRafRef.current) cancelAnimationFrame(resizeRafRef.current);
};
}, []);
useEffect(() => {
// NEW: while resizing, dont regenerate columns
if (isResizing) return;
// NEW: bail early BEFORE expensive ProductionListColumns(...) call
if (!_.isEqual(initialColumnsRef.current, columns)) return;
const newColumns = const newColumns =
matchingColumnConfig?.columns.columnKeys.map((k) => { matchingColumnConfig?.columns.columnKeys.map((k) => {
return { return {
@@ -152,10 +179,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
width: k.width ?? 100 width: k.width ?? 100
}; };
}) || []; }) || [];
// Only update columns if they haven't been manually changed by the user
if (_.isEqual(initialColumnsRef.current, columns)) {
setColumns(newColumns); setColumns(newColumns);
}
}, [ }, [
matchingColumnConfig, matchingColumnConfig,
bodyshop, bodyshop,
@@ -165,7 +190,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
Production_List_Status_Colors, Production_List_Status_Colors,
refetch, refetch,
state, state,
columns columns,
isResizing
]); ]);
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -215,19 +241,54 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
logImEXEvent("production_list_remove_column", { key }); logImEXEvent("production_list_remove_column", { key });
}; };
const handleResize = // NEW: commit widths via rAF (less jank)
(index) => const applyColumnWidth = useCallback((columnKey, width) => {
setColumns((prev) => {
const idx = prev.findIndex((c) => c.key === columnKey);
if (idx === -1) return prev;
const currentWidth = prev[idx].width ?? 100;
if (currentWidth === width) return prev;
const next = prev.slice();
next[idx] = { ...next[idx], width };
return next;
});
}, []);
const handleResize = useCallback(
(columnKey) =>
(e, { size }) => { (e, { size }) => {
const nextColumns = [...columns]; pendingResizeRef.current = { columnKey, width: size.width };
nextColumns[index] = {
...nextColumns[index], if (resizeRafRef.current) return;
width: size.width resizeRafRef.current = requestAnimationFrame(() => {
}; resizeRafRef.current = null;
if (!_.isEqual(nextColumns, columns)) { const pending = pendingResizeRef.current;
setColumns(nextColumns); if (!pending) return;
applyColumnWidth(pending.columnKey, pending.width);
});
},
[applyColumnWidth]
);
const handleResizeStart = useCallback(() => {
setIsResizing(true);
}, []);
const handleResizeStop = useCallback(
(columnKey) =>
(e, { size }) => {
setIsResizing(false);
// Ensure final width is committed
applyColumnWidth(columnKey, size.width);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
} logImEXEvent("production_list_resize_column", { key: columnKey, width: size.width });
}; },
[applyColumnWidth]
);
const addColumn = (newColumn) => { const addColumn = (newColumn) => {
const updatedColumns = [...columns, newColumn]; const updatedColumns = [...columns, newColumn];
@@ -384,6 +445,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
onSave={() => { onSave={() => {
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
initialStateRef.current = state; initialStateRef.current = state;
// NEW: after saving, treat current columns as the baseline
initialColumnsRef.current = columns;
}} }}
/> />
<Input <Input
@@ -410,7 +474,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
tableLayout="fixed" tableLayout="fixed"
pagination={false} pagination={false}
size="small" size="small"
{...(Production_List_Status_Colors.treatment === "on" && { {...(Production_List_Status_Colors.treatment === "on" &&
!isResizing && {
onRow: (record, index) => { onRow: (record, index) => {
if (!bodyshop.md_ro_statuses.production_colors) return null; if (!bodyshop.md_ro_statuses.production_colors) return null;
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status); const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
@@ -438,7 +503,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
cell: DraggableHeaderCell cell: DraggableHeaderCell
} }
}} }}
columns={columns.map((c, index) => { columns={columns.map((c) => {
return { return {
...c, ...c,
filteredValue: state.filteredInfo[c.key] || null, filteredValue: state.filteredInfo[c.key] || null,
@@ -449,7 +514,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
onHeaderCell: (column) => ({ onHeaderCell: (column) => ({
columnKey: column.key, columnKey: column.key,
width: column.width, width: column.width,
onResize: handleResize(index) onResize: handleResize(column.key),
onResizeStart: handleResizeStart,
onResizeStop: handleResizeStop(column.key)
}) })
}; };
})} })}
@@ -460,6 +527,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
onChange={handleTableChange} onChange={handleTableChange}
/> />
</SortableContext> </SortableContext>
<DragOverlay dropAnimation={null}> <DragOverlay dropAnimation={null}>
{activeId ? ( {activeId ? (
<div <div

View File

@@ -1,8 +1,9 @@
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Resizable } from "react-resizable"; import { Resizable } from "react-resizable";
import "react-resizable/css/styles.css";
const ResizableComponent = forwardRef((props, ref) => { const ResizableComponent = forwardRef((props, ref) => {
const { onResize, width, dragAttributes, dragListeners, ...restProps } = props; const { onResize, onResizeStart, onResizeStop, width, dragAttributes, dragListeners, ...restProps } = props;
if (!width) { if (!width) {
return <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />; return <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />;
@@ -10,11 +11,16 @@ const ResizableComponent = forwardRef((props, ref) => {
return ( return (
<Resizable <Resizable
width={width || 200} width={width}
height={0} height={0}
onResize={onResize} onResize={onResize}
onResizeStart={onResizeStart}
onResizeStop={onResizeStop}
draggableOpts={{ enableUserSelectHack: false }} draggableOpts={{ enableUserSelectHack: false }}
handle={<span className="react-resizable-handle" />} resizeHandles={["e"]}
handle={(axis, handleRef) => (
<span ref={handleRef} className={`react-resizable-handle react-resizable-handle-${axis}`} />
)}
> >
<th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} /> <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />
</Resizable> </Resizable>