Compare commits

..

3 Commits

Author SHA1 Message Date
Allan Carr
166a33af4e IO-3376 Scrollbar Theming
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-09-17 16:07:15 -07:00
Allan Carr
99f425eac4 Merged in hotfix/2025-09-17 (pull request #2571)
IO-3373 Dashboard Component Infinite Recursion
2025-09-17 21:09:03 +00:00
Allan Carr
b2c504c69d Merged in feature/IO-3373-Dashboard-Component-Infinite-Recursion (pull request #2570)
IO-3373 Dashboard Component Infinite Recursion
2025-09-17 21:01:11 +00:00
3 changed files with 54 additions and 93 deletions

View File

@@ -272,23 +272,23 @@
}
// Scrollbar styles (uncomment if needed, updated for dark mode)
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 0.2rem;
background-color: var(--table-stripe-bg);
}
// ::-webkit-scrollbar-track {
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// border-radius: 0.2rem;
// background-color: var(--table-stripe-bg);
// }
::-webkit-scrollbar {
width: 0.25rem;
max-height: 0.25rem;
background-color: var(--table-stripe-bg);
}
// ::-webkit-scrollbar {
// width: 0.25rem;
// max-height: 0.25rem;
// background-color: var(--table-stripe-bg);
// }
::-webkit-scrollbar-thumb {
border-radius: 0.2rem;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: var(--alert-color);
}
// ::-webkit-scrollbar-thumb {
// border-radius: 0.2rem;
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// background-color: var(--alert-color);
// }
.ant-input-number-input,
.ant-input-number,

View File

@@ -2,13 +2,11 @@ import { gql } from "@apollo/client";
import dayjs from "../../utils/day.js";
import componentList from "./componentList.js";
const createDashboardQuery = (items) => {
const createDashboardQuery = (state) => {
const componentBasedAdditions =
Array.isArray(items) &&
items
.map((item) => (componentList[item.i] && componentList[item.i].gqlFragment) || "")
.filter(Boolean)
.join("");
state &&
Array.isArray(state.layout) &&
state.layout.map((item) => componentList[item.i].gqlFragment || "").join("");
return gql`
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [

View File

@@ -1,5 +1,5 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
import { cloneDeep } from "lodash";
import { cloneDeep, isEmpty } from "lodash";
import { useMutation, useQuery } from "@apollo/client";
import { Button, Dropdown, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
@@ -34,25 +34,14 @@ const mapDispatchToProps = () => ({
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 [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout
: { items: [], layout: {}, layouts: [] })
});
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, {
const { loading, error, data, refetch } = useQuery(createDashboardQuery(state), {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
@@ -60,32 +49,21 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleLayoutChange = async (layout, layouts) => {
try {
logImEXEvent("dashboard_change_layout");
logImEXEvent("dashboard_change_layout");
setState((prev) => ({ ...prev, layout, layouts }));
setState({ ...state, 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("; ")
})
});
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: { ...state, layout, layouts }
}
} catch (err) {
// Catch any unexpected errors (including potential cyclic JSON issues) so the promise never rejects unhandled
console.error("Dashboard layout update failed", err);
});
if (!isEmpty(result?.errors)) {
notification.error({
message: t("dashboard.errors.updatinglayout", {
message: err?.message || String(err)
message: JSON.stringify(result.errors)
})
});
}
@@ -102,26 +80,19 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
};
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,
logImEXEvent("dashboard_add_component", { name: e.key });
setState({
...state,
items: [
...state.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)
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2
}
];
return { ...prev, items: nextItems };
]
});
};
@@ -159,33 +130,25 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
className="layout"
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
>
{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)
};
const TheComponent = componentList[item.i].component;
return (
<div
key={safeItem.i}
key={item.i}
data-grid={{
...safeItem,
minH,
minW
...item,
minH: componentList[item.i].minH || 1,
minW: componentList[item.i].minW || 1
}}
>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={safeItem.i}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
@@ -193,9 +156,9 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
top: ".25rem",
cursor: "pointer"
}}
onClick={() => handleRemoveComponent(safeItem.i)}
onClick={() => handleRemoveComponent(item.i)}
/>
{TheComponent && <TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />}
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />
</LoadingSkeleton>
</div>
);