import Icon, { SyncOutlined } from "@ant-design/icons"; import { cloneDeep } from "lodash"; import { useMutation, useQuery } from "@apollo/client"; import { Button, Dropdown, Space } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; import { useMemo, useState } from "react"; import { Responsive, WidthProvider } from "react-grid-layout"; import { useTranslation } from "react-i18next"; import { MdClose } from "react-icons/md"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import { GenerateDashboardData } from "./dashboard-grid.utils"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import componentList from "./componentList.js"; import createDashboardQuery from "./createDashboardQuery.js"; import "./dashboard-grid.styles.scss"; const ResponsiveReactGridLayout = WidthProvider(Responsive); const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, bodyshop: selectBodyshop }); const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export function DashboardGridComponent({ currentUser, bodyshop }) { const { t } = useTranslation(); const [state, setState] = useState(() => { const persisted = bodyshop.associations[0].user.dashboardlayout; // Normalize persisted structure to avoid malformed shapes that can cause recursive layout recalculations if (persisted) { return { items: Array.isArray(persisted.items) ? persisted.items : [], layout: Array.isArray(persisted.layout) ? persisted.layout : [], layouts: typeof persisted.layouts === "object" && !Array.isArray(persisted.layouts) ? persisted.layouts : {}, cols: persisted.cols }; } return { items: [], layout: [], layouts: {}, cols: 12 }; }); const notification = useNotification(); // Memoize the query document so Apollo doesn't treat each render as a brand-new query causing continuous re-fetches const dashboardQueryDoc = useMemo(() => createDashboardQuery(state.items), [state.items]); const { loading, error, data, refetch } = useQuery(dashboardQueryDoc, { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); const handleLayoutChange = async (layout, layouts) => { try { logImEXEvent("dashboard_change_layout"); setState((prev) => ({ ...prev, layout, layouts })); const result = await updateLayout({ variables: { email: currentUser.email, layout: { ...state, layout, layouts } } }); if (result?.errors && result.errors.length) { const errorMessages = result.errors.map((e) => e?.message || String(e)); notification.error({ message: t("dashboard.errors.updatinglayout", { message: errorMessages.join("; ") }) }); } } catch (err) { // Catch any unexpected errors (including potential cyclic JSON issues) so the promise never rejects unhandled console.error("Dashboard layout update failed", err); notification.error({ message: t("dashboard.errors.updatinglayout", { message: err?.message || String(err) }) }); } }; const handleRemoveComponent = (key) => { logImEXEvent("dashboard_remove_component", { name: key }); const idxToRemove = state.items.findIndex((i) => i.i === key); const items = cloneDeep(state.items); items.splice(idxToRemove, 1); setState({ ...state, items }); }; const handleAddComponent = (e) => { // Avoid passing the full AntD menu click event (contains circular refs) to analytics logImEXEvent("dashboard_add_component", { key: e.key }); const compSpec = componentList[e.key] || {}; const minW = compSpec.minW || 1; const minH = compSpec.minH || 1; const baseW = compSpec.w || 2; const baseH = compSpec.h || 2; setState((prev) => { const nextItems = [ ...prev.items, { i: e.key, // Position near bottom: use a large y so RGL places it last without triggering cascading relayout loops x: (prev.items.length * 2) % (prev.cols || 12), y: 1000, w: Math.max(baseW, minW), h: Math.max(baseH, minH) } ]; return { ...prev, items: nextItems }; }); }; const dashboardData = useMemo(() => GenerateDashboardData(data), [data]); const existingLayoutKeys = state.items.map((i) => i.i); const menuItems = Object.keys(componentList).map((key) => ({ key: key, label: componentList[key].label, value: key, disabled: existingLayoutKeys.includes(key) })); const menu = { items: menuItems, onClick: handleAddComponent }; if (error) return ; return (
} /> {state.items.map((item) => { const spec = componentList[item.i] || {}; const TheComponent = spec.component; const minW = spec.minW || 1; const minH = spec.minH || 1; // Ensure current width/height respect minimums to avoid react-grid-layout prop warnings const safeItem = { ...item, w: Math.max(item.w || spec.w || minW, minW), h: Math.max(item.h || spec.h || minH, minH) }; return (
handleRemoveComponent(safeItem.i)} /> {TheComponent && }
); })}
); } export default connect(mapStateToProps, mapDispatchToProps)(DashboardGridComponent);