Merged in feature/IO-2882-Unsaved-Listview-Changes (pull request #1612)

- Production Board List View Unsaved Changes Prompt

Approved-by: Allan Carr
This commit is contained in:
Dave Richer
2024-08-16 00:45:48 +00:00
committed by Allan Carr
4 changed files with 228 additions and 128 deletions

View File

@@ -2,7 +2,6 @@ import React from "react";
import { Button, Dropdown } from "antd"; import { Button, Dropdown } from "antd";
import dataSource from "./production-list-columns.data"; import dataSource from "./production-list-columns.data";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
@@ -10,16 +9,23 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
technician: selectTechnician, technician: selectTechnician,
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ProductionColumnsComponent);
export function ProductionColumnsComponent({ columnState, technician, bodyshop, data, tableState, refetch }) { const mapDispatchToProps = (dispatch) => ({
// Add any necessary dispatch actions here
});
export function ProductionColumnsComponent({
columnState,
technician,
bodyshop,
data,
tableState,
refetch,
onColumnAdd
}) {
const [columns, setColumns] = columnState; const [columns, setColumns] = columnState;
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
@@ -29,18 +35,26 @@ export function ProductionColumnsComponent({ columnState, technician, bodyshop,
names: ["Enhanced_Payroll"], names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid splitKey: bodyshop.imexshopid
}); });
const handleAdd = (e) => { const handleAdd = (e) => {
setColumns([ const newColumn = dataSource({
...columns, bodyshop,
...dataSource({ technician,
bodyshop, state: tableState,
technician, data,
state: tableState, activeStatuses: bodyshop.md_ro_statuses.active_statuses,
data, treatments: { Enhanced_Payroll }
activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((i) => i.key === e.key);
treatments: { Enhanced_Payroll }
}).filter((i) => i.key === e.key) if (newColumn) {
]); const updatedColumns = [...columns, newColumn];
setColumns(updatedColumns);
// Call the onColumnAdd function passed as a prop
if (onColumnAdd) {
onColumnAdd(newColumn);
}
}
}; };
const columnKeys = columns.map((i) => i.key); const columnKeys = columns.map((i) => i.key);
@@ -76,12 +90,4 @@ export function ProductionColumnsComponent({ columnState, technician, bodyshop,
); );
} }
// <Transfer export default connect(mapStateToProps, mapDispatchToProps)(ProductionColumnsComponent);
// dataSource={dataSource}
// titles={["Source", "Target"]}
// targetKeys={columns.map((c) => c.key)}
// render={(item) => item.title}
// onChange={(nextTargetKeys, direction, moveKeys) => {
// setColumns(dataSource.filter((i) => nextTargetKeys.includes(i.key)));
// }}
// />

View File

@@ -7,6 +7,7 @@ import { Button, Form, Input, notification, Popover, Space } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries"; import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { isFunction } from "lodash";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -16,7 +17,7 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export function ProductionListSaveConfigButton({ columns, bodyshop, tableState }) { export function ProductionListSaveConfigButton({ columns, bodyshop, tableState, onSave }) {
const [updateShop] = useMutation(UPDATE_SHOP); const [updateShop] = useMutation(UPDATE_SHOP);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -49,6 +50,9 @@ export function ProductionListSaveConfigButton({ columns, bodyshop, tableState }
}); });
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bodyshop.successes.save") }); notification["success"]({ message: t("bodyshop.successes.save") });
if (onSave && isFunction(onSave)) {
onSave();
}
} else { } else {
notification["error"]({ notification["error"]({
message: t("bodyshop.errors.saving", { message: t("bodyshop.errors.saving", {

View File

@@ -11,6 +11,7 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionListColumns from "../production-list-columns/production-list-columns.data"; import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { isFunction } from "lodash";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -18,7 +19,17 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser currentUser: selectCurrentUser
}); });
export function ProductionListTable({ refetch, bodyshop, technician, currentUser, state, data, setColumns, setState }) { export function ProductionListTable({
refetch,
bodyshop,
technician,
currentUser,
state,
data,
setColumns,
setState,
onProfileChange
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW); const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
const [updateShop] = useMutation(UPDATE_SHOP); const [updateShop] = useMutation(UPDATE_SHOP);
@@ -32,25 +43,25 @@ export function ProductionListTable({ refetch, bodyshop, technician, currentUser
}); });
const handleSelect = async (value, option) => { const handleSelect = async (value, option) => {
setColumns( const newColumns = bodyshop.production_config
bodyshop.production_config .filter((pc) => pc.name === value)[0]
.filter((pc) => pc.name === value)[0] .columns.columnKeys.map((k) => {
.columns.columnKeys.map((k) => { return {
return { ...ProductionListColumns({
...ProductionListColumns({ bodyshop,
bodyshop, refetch,
refetch, technician,
technician, state,
state, data: data,
data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses,
activeStatuses: bodyshop.md_ro_statuses.active_statuses, treatments: { Enhanced_Payroll }
treatments: { Enhanced_Payroll } }).find((e) => e.key === k.key),
}).find((e) => e.key === k.key), width: k.width
width: k.width };
}; });
}) setColumns(newColumns);
); const newState = bodyshop.production_config.filter((pc) => pc.name === value)[0].columns.tableState;
setState(bodyshop.production_config.filter((pc) => pc.name === value)[0].columns.tableState); setState(newState);
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email); const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
@@ -72,6 +83,10 @@ export function ProductionListTable({ refetch, bodyshop, technician, currentUser
} }
}); });
} }
if (onProfileChange && isFunction(onProfileChange)) {
onProfileChange({ value, option, newColumns, newState, assoc });
}
}; };
const handleTrash = async (name) => { const handleTrash = async (name) => {

View File

@@ -1,8 +1,6 @@
import { SyncOutlined } from "@ant-design/icons"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd"; import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import React, { useEffect, useMemo, useState } from "react";
import ReactDragListView from "react-drag-listview"; import ReactDragListView from "react-drag-listview";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -16,6 +14,11 @@ import ProductionListSaveConfigButton from "../production-list-save-config-butto
import ProductionListPrint from "./production-list-print.component"; import ProductionListPrint from "./production-list-print.component";
import ProductionListTableViewSelect from "./production-list-table-view-select.component"; import ProductionListTableViewSelect from "./production-list-table-view-select.component";
import ResizeableTitle from "./production-list-table.resizeable.component"; import ResizeableTitle from "./production-list-table.resizeable.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { SyncOutlined } from "@ant-design/icons";
import Prompt from "../../utils/prompt.js";
import _ from "lodash";
import AlertComponent from "../alert/alert.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -25,6 +28,7 @@ const mapStateToProps = createStructuredSelector({
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) { export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const { const {
treatments: { Production_List_Status_Colors, Enhanced_Payroll } treatments: { Production_List_Status_Colors, Enhanced_Payroll }
@@ -35,10 +39,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
}); });
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email); const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
const defaultView = assoc && assoc.default_prod_list_view; const defaultView = assoc && assoc.default_prod_list_view;
const [state, setState] = useState( 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) ||
bodyshop.production_config[0]?.columns.tableState || { bodyshop.production_config[0]?.columns.tableState || {
@@ -47,80 +50,102 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
} }
); );
const { t } = useTranslation(); const initialColumnsRef = useRef(
(initialStateRef.current &&
const matchingColumnConfig = useMemo(() => { bodyshop.production_config
return bodyshop.production_config.find((p) => p.name === defaultView); .find((p) => p.name === defaultView)
}, [bodyshop.production_config, defaultView]); ?.columns.columnKeys.map((k) => {
const [columns, setColumns] = useState(
(state &&
matchingColumnConfig &&
matchingColumnConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state,
data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width ?? 100
};
})) ||
[]
);
useEffect(() => {
const newColumns =
(state &&
matchingColumnConfig &&
matchingColumnConfig.columns.columnKeys.map((k) => {
return { return {
...ProductionListColumns({ ...ProductionListColumns({
bodyshop, bodyshop,
technician,
refetch, refetch,
state, technician,
data: data, state: initialStateRef.current,
data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses, activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Production_List_Status_Colors, Enhanced_Payroll } treatments: { Production_List_Status_Colors, Enhanced_Payroll }
}).find((e) => e.key === k.key), }).find((e) => e.key === k.key),
width: k.width ?? 100 width: k.width ?? 100
}; };
})) || })) ||
[]; []
setColumns(newColumns); );
// eslint-disable-next-line react-hooks/exhaustive-deps
const [state, setState] = useState(initialStateRef.current);
const [columns, setColumns] = useState(initialColumnsRef.current);
const { t } = useTranslation();
const matchingColumnConfig = useMemo(() => {
return bodyshop.production_config.find((p) => p.name === defaultView);
}, [bodyshop.production_config, defaultView]);
useEffect(() => {
const newColumns =
matchingColumnConfig?.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
technician,
refetch,
state,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width ?? 100
};
}) || [];
// Only update columns if they haven't been manually changed by the user
if (_.isEqual(initialColumnsRef.current, columns)) {
setColumns(newColumns);
}
}, [ }, [
//state,
matchingColumnConfig, matchingColumnConfig,
bodyshop, bodyshop,
technician, technician,
data data,
]); //State removed from dependency array as it causes race condition when removing columns from table view and is not needed. Enhanced_Payroll,
Production_List_Status_Colors,
refetch,
state,
columns
]);
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ const newState = {
...state, ...state,
filteredInfo: filters, filteredInfo: filters,
sortedInfo: { columnKey: sorter.columnKey, order: sorter.order } sortedInfo: { columnKey: sorter.columnKey, order: sorter.order }
}); };
if (!_.isEqual(newState, state)) {
setState(newState);
setHasUnsavedChanges(true);
}
}; };
const onDragEnd = (fromIndex, toIndex) => { const onDragEnd = (fromIndex, toIndex) => {
const columnsCopy = columns.slice(); if (fromIndex === toIndex) return;
const item = columnsCopy.splice(fromIndex, 1)[0];
columnsCopy.splice(toIndex, 0, item); const columnsCopy = [...columns];
setColumns(columnsCopy); const [movedItem] = columnsCopy.splice(fromIndex, 1);
columnsCopy.splice(toIndex, 0, movedItem);
if (!_.isEqual(columnsCopy, columns)) {
setColumns(columnsCopy);
setHasUnsavedChanges(true);
}
}; };
const removeColumn = (e) => { const removeColumn = (e) => {
const { key } = e; const { key } = e;
const newColumns = columns.filter((i) => i.key !== key); const newColumns = columns.filter((i) => i.key !== key);
setColumns(newColumns);
if (!_.isEqual(newColumns, columns)) {
setColumns(newColumns);
setHasUnsavedChanges(true);
}
}; };
const handleResize = const handleResize =
@@ -131,9 +156,21 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
...nextColumns[index], ...nextColumns[index],
width: size.width width: size.width
}; };
setColumns(nextColumns);
if (!_.isEqual(nextColumns, columns)) {
setColumns(nextColumns);
setHasUnsavedChanges(true);
}
}; };
const addColumn = (newColumn) => {
const updatedColumns = [...columns, newColumn];
if (!_.isEqual(updatedColumns, columns)) {
setColumns(updatedColumns);
setHasUnsavedChanges(true);
}
};
const headerItem = (col) => { const headerItem = (col) => {
const menu = { const menu = {
onClick: removeColumn, onClick: removeColumn,
@@ -152,29 +189,29 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
); );
}; };
const dataSource = const resetChanges = () => {
searchText === "" setState(initialStateRef.current);
? data setColumns(initialColumnsRef.current);
: data.filter( setHasUnsavedChanges(false);
(j) => };
(j.ro_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.ownr_fn || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.ownr_ln || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.status || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.ins_co_nm || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.v_model_desc || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.v_make_desc || "").toLowerCase().includes(searchText.toLowerCase())
);
// const handleSelectRecord = (record) => { const filterData = (item, searchText) => {
// if (selected !== record.id) { const fieldsToSearch = [
// setSelected(record.id); item.ro_number,
// } else { item.ownr_co_nm,
// setSelected(null); item.ownr_fn,
// } item.ownr_ln,
// }; item.status,
item.ins_co_nm,
item.clm_no,
item.v_model_desc,
item.v_make_desc
];
return fieldsToSearch.some((field) => (field || "").toString().toLowerCase().includes(searchText.toLowerCase()));
};
const dataSource = searchText === "" ? data : data.filter((j) => filterData(j, searchText));
if (!!!columns) return <div>No columns found.</div>; if (!!!columns) return <div>No columns found.</div>;
@@ -186,8 +223,29 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
.toFixed(1); .toFixed(1);
const totalLAB = data.reduce((acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1); const totalLAB = data.reduce((acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
const totalLAR = data.reduce((acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1); const totalLAR = data.reduce((acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
return ( return (
<div> <div>
<Prompt when={hasUnsavedChanges} beforeUnload={true} message={t("general.messages.unsavedchangespopup")} />
{hasUnsavedChanges && (
<AlertComponent
type="warning"
message={
<div>
<span>{t("general.messages.unsavedchanges")} </span>
<span
onClick={resetChanges}
style={{
cursor: "pointer",
textDecoration: "underline"
}}
>
{t("general.actions.reset")}
</span>
</div>
}
/>
)}
<PageHeader <PageHeader
title={ title={
<Space> <Space>
@@ -199,20 +257,37 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
} }
extra={ extra={
<Space wrap> <Space wrap>
<Button onClick={() => refetch && refetch()}> <Button
onClick={() => {
refetch && refetch();
}}
>
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
<ProductionListColumnsAdd columnState={[columns, setColumns]} tableState={state} data={data} /> <ProductionListColumnsAdd
<ProductionListSaveConfigButton columns={columns} tableState={state} /> columnState={[columns, setColumns]}
tableState={state}
data={data}
onColumnAdd={addColumn}
/>
<ProductionListSaveConfigButton
columns={columns}
tableState={state}
onSave={() => {
setHasUnsavedChanges(false);
}}
/>
<ProductionListTableViewSelect <ProductionListTableViewSelect
state={state} state={state}
setState={setState} setState={setState}
setColumns={setColumns} setColumns={setColumns}
onProfileChange={() => {
initialStateRef.current = state;
setHasUnsavedChanges(false);
}}
refetch={refetch} refetch={refetch}
data={data} data={data}
/> />
<Input <Input
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}