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) => {
(e, { size }) => { setColumns((prev) => {
const nextColumns = [...columns]; const idx = prev.findIndex((c) => c.key === columnKey);
nextColumns[index] = { if (idx === -1) return prev;
...nextColumns[index],
width: size.width const currentWidth = prev[idx].width ?? 100;
}; if (currentWidth === width) return prev;
if (!_.isEqual(nextColumns, columns)) {
setColumns(nextColumns); const next = prev.slice();
next[idx] = { ...next[idx], width };
return next;
});
}, []);
const handleResize = useCallback(
(columnKey) =>
(e, { size }) => {
pendingResizeRef.current = { columnKey, width: size.width };
if (resizeRafRef.current) return;
resizeRafRef.current = requestAnimationFrame(() => {
resizeRafRef.current = null;
const pending = pendingResizeRef.current;
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,35 +474,36 @@ 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" &&
onRow: (record, index) => { !isResizing && {
if (!bodyshop.md_ro_statuses.production_colors) return null; onRow: (record, index) => {
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status); if (!bodyshop.md_ro_statuses.production_colors) return null;
if (!color) { const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
if (index % 2 === 0) if (!color) {
return { if (index % 2 === 0)
style: { return {
backgroundColor: "var(--table-row-even-bg)" style: {
} backgroundColor: "var(--table-row-even-bg)"
}; }
return null; };
} return null;
return {
className: "rowWithColor",
style: {
"--bgColor": color.color
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
: "var(--status-row-bg-fallback)"
} }
}; return {
} className: "rowWithColor",
})} style: {
"--bgColor": color.color
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
: "var(--status-row-bg-fallback)"
}
};
}
})}
components={{ components={{
header: { header: {
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>