feature/IO-3545-Production-Board-List-DND - Checkpoint
This commit is contained in:
@@ -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, don’t 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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user