diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx
index 929681bbd..8a214051e 100644
--- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx
+++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx
@@ -1,17 +1,17 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
-import { cloneDeep } from "lodash";
-import { useMutation, useQuery } from "@apollo/client";
+import { useMutation, useQuery, useApolloClient } from "@apollo/client";
import { Button, Dropdown, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
-import { useMemo, useState } from "react";
+import { useMemo, useState, useEffect } 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 { UPDATE_DASHBOARD_LAYOUT, QUERY_USER_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
+import { QUERY_DASHBOARD_BODYSHOP } from "../../graphql/bodyshop.queries";
+import { 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";
@@ -24,128 +24,185 @@ import "./dashboard-grid.styles.scss";
const ResponsiveReactGridLayout = WidthProvider(Responsive);
const mapStateToProps = createStructuredSelector({
- currentUser: selectCurrentUser,
- bodyshop: selectBodyshop
+ currentUser: selectCurrentUser
});
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export function DashboardGridComponent({ currentUser, bodyshop }) {
+export function DashboardGridComponent({ currentUser }) {
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 client = useApolloClient();
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]);
+ // Constants for layout defaults
+ const DEFAULT_COLS = 12;
+ const DEFAULT_Y_POSITION = 1000;
+ const GRID_BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 };
+ const GRID_COLS = { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 };
- const { loading, error, data, refetch } = useQuery(dashboardQueryDoc, {
+ // Fetch dashboard layout data
+ const { data: layoutData } = useQuery(QUERY_USER_DASHBOARD_LAYOUT, {
+ variables: { email: currentUser.email },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ skip: !currentUser?.email
+ });
+
+ // Fetch minimal bodyshop data for components
+ const {
+ loading,
+ error,
+ data: bodyshopData
+ } = useQuery(QUERY_DASHBOARD_BODYSHOP, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
- const handleLayoutChange = async (layout, layouts) => {
+ // Memoize layout state initialization
+ const initialState = useMemo(() => {
+ const persisted = layoutData?.users?.[0]?.dashboardlayout;
+ if (persisted) {
+ const { items = [], layout = [], layouts = {}, cols = DEFAULT_COLS } = persisted;
+ return {
+ items: Array.isArray(items) ? items : [],
+ layout: Array.isArray(layout) ? layout : [],
+ layouts: typeof layouts === "object" && !Array.isArray(layouts) ? layouts : {},
+ cols
+ };
+ }
+ return { items: [], layout: [], layouts: {}, cols: DEFAULT_COLS };
+ }, [layoutData]);
+
+ const [state, setState] = useState(initialState);
+
+ // Update state when layout data changes
+ useEffect(() => {
+ if (layoutData?.users?.[0]?.dashboardlayout) {
+ const { items = [], layout = [], layouts = {}, cols = DEFAULT_COLS } = layoutData.users[0].dashboardlayout;
+ setState({
+ items: Array.isArray(items) ? items : [],
+ layout: Array.isArray(layout) ? layout : [],
+ layouts: typeof layouts === "object" && !Array.isArray(layouts) ? layouts : {},
+ cols
+ });
+ }
+ }, [layoutData]);
+
+ // Get bodyshop data for components
+ const bodyshop = bodyshopData?.dashboard_bodyshops?.[0];
+
+ // DRY helper function to update layout in database and cache
+ const updateLayoutAndCache = async (updatedLayout, errorContext = "updating layout") => {
try {
- logImEXEvent("dashboard_change_layout");
-
- setState((prev) => ({ ...prev, layout, layouts }));
-
- const result = await updateLayout({
- variables: {
- email: currentUser.email,
- layout: { ...state, layout, layouts }
- }
+ const { data: result } = await updateLayout({
+ variables: { email: currentUser.email, layout: updatedLayout }
});
- if (result?.errors && result.errors.length) {
- const errorMessages = result.errors.map((e) => e?.message || String(e));
+ const { errors = [] } = result?.update_users?.returning?.[0] || {};
+
+ if (errors.length) {
+ const errorMessages = errors.map(({ message }) => message || String(error));
notification.error({
message: t("dashboard.errors.updatinglayout", {
message: errorMessages.join("; ")
})
});
+ return false;
}
+
+ // Note: Removed Apollo cache update to prevent triggering unwanted Redux actions
+ // Instead, evict the dashboard bodyshop query from cache to ensure fresh data on next fetch
+ client.cache.evict({ fieldName: "dashboard_bodyshops" });
+ client.cache.gc();
+
+ return true;
} catch (err) {
- // Catch any unexpected errors (including potential cyclic JSON issues) so the promise never rejects unhandled
- console.error("Dashboard layout update failed", err);
+ console.error(`Dashboard ${errorContext} failed`, err);
notification.error({
message: t("dashboard.errors.updatinglayout", {
message: err?.message || String(err)
})
});
+ return false;
}
};
- const handleRemoveComponent = (key) => {
+ // 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: dashboardLoading,
+ error: dashboardError,
+ data: dashboardQueryData,
+ refetch
+ } = useQuery(dashboardQueryDoc, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
+
+ const dashboardData = useMemo(() => GenerateDashboardData(dashboardQueryData), [dashboardQueryData]);
+
+ // Memoize existing layout keys to prevent unnecessary recalculations
+ const existingLayoutKeys = useMemo(() => state.items.map(({ i }) => i), [state.items]);
+
+ // Memoize menu items to prevent unnecessary recalculations
+ const menuItems = useMemo(
+ () =>
+ Object.entries(componentList).map(([key, { label }]) => ({
+ key,
+ label,
+ value: key,
+ disabled: existingLayoutKeys.includes(key)
+ })),
+ [existingLayoutKeys]
+ );
+
+ if (loading || dashboardLoading) return