diff --git a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
index 83fed88e3..11deaa01d 100644
--- a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
+++ b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
@@ -1,4 +1,4 @@
-import React, { Suspense, useState, useEffect, useMemo } from "react";
+import React, { Suspense, useState, useEffect, useMemo, useRef } from "react";
import { useNavigate, useParams } from "react-router";
import { useDispatch } from "react-redux";
@@ -42,6 +42,7 @@ export default function BingoGameDataAdmin(props) {
const { t, i18n } = useTranslation(`${category}s`);
const [dirty, setDirty] = useState(false);
+ const hasLoadedRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [messages, setMessages] = useState({});
@@ -86,6 +87,10 @@ export default function BingoGameDataAdmin(props) {
}, [endpointStatus]);
useEffect(() => {
+ if (!hasLoadedRef.current) {
+ hasLoadedRef.current = true;
+ return;
+ }
setDirty(true);
}, [
gameTopic,
@@ -162,6 +167,7 @@ export default function BingoGameDataAdmin(props) {
setGameGroupDiscount(bingo_game.group_discount || 0);
setGameGroupProjectId(bingo_game.project_id);
setFoundWords(data.found_words);
+ setDirty(false);
//getBingoGameData();
//setDirty(false);
@@ -201,7 +207,6 @@ export default function BingoGameDataAdmin(props) {
};
const getBingoGameData = () => {
- setDirty(true);
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === bingoGameId) {
diff --git a/app/javascript/components/BingoBoards/CandidateListEntry.tsx b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
index 7b4d49f1d..e75e46158 100644
--- a/app/javascript/components/BingoBoards/CandidateListEntry.tsx
+++ b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { useParams } from "react-router";
import { Panel } from "primereact/panel";
@@ -40,6 +40,7 @@ export default function CandidateListEntry(props: Props) {
const { bingoGameId } = useParams();
const [dirty, setDirty] = useState(false);
+ const hasLoadedRef = useRef(false);
const dispatch = useDispatch();
useDirtyStatus(category, dirty);
@@ -59,7 +60,6 @@ export default function CandidateListEntry(props: Props) {
const getCandidateList = () => {
dispatch(startTask());
- setDirty(true);
const url =
props.rootPath === undefined
? `${endpoints.baseUrl}${bingoGameId}.json`
@@ -184,6 +184,10 @@ export default function CandidateListEntry(props: Props) {
}, [endpointStatus]);
useEffect(() => {
+ if (!hasLoadedRef.current) {
+ hasLoadedRef.current = true;
+ return;
+ }
setDirty(true);
}, [candidates]);
diff --git a/app/javascript/components/assignments/AssignmentDataAdmin.tsx b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
index b856261f0..573e45ad6 100644
--- a/app/javascript/components/assignments/AssignmentDataAdmin.tsx
+++ b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
@@ -1,4 +1,4 @@
-import React, { Suspense, useState, useEffect } from "react";
+import React, { Suspense, useState, useEffect, useRef } from "react";
import { useParams } from "react-router";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
@@ -44,6 +44,7 @@ export default function AssignmentDataAdmin(props) {
const navigate = useNavigate();
const [dirty, setDirty] = useState(false);
+ const hasLoadedRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [assignmentProjects, setAssignmentProjects] = useState([
@@ -99,6 +100,10 @@ export default function AssignmentDataAdmin(props) {
}, [endpointStatus]);
useEffect(() => {
+ if (!hasLoadedRef.current) {
+ hasLoadedRef.current = true;
+ return;
+ }
setDirty(true);
}, [
assignmentName,
@@ -213,7 +218,6 @@ export default function AssignmentDataAdmin(props) {
setAssignmentRubricId(assignment.rubric_id || -1);
};
const getAssignmentData = () => {
- setDirty(true);
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === assignmentId) {
diff --git a/app/javascript/components/assignments/AssignmentSubmission.tsx b/app/javascript/components/assignments/AssignmentSubmission.tsx
index 6bf0cb5aa..7f635e5c7 100644
--- a/app/javascript/components/assignments/AssignmentSubmission.tsx
+++ b/app/javascript/components/assignments/AssignmentSubmission.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useMemo } from "react";
+import React, { useState, useEffect, useMemo, useRef } from "react";
import { useNavigate } from "react-router";
import { Temporal, TemporalSettings as Settings, parseISO } from "../infrastructure/TemporalSettings";
@@ -43,6 +43,7 @@ export default function AssignmentSubmission(props: Props) {
const navigate = useNavigate();
const [t, i18n] = useTranslation(`${category}s`);
const [dirty, setDirty] = useState(false);
+ const hasLoadedRef = useRef(false);
useDirtyStatus(category, dirty);
const [submissionId, setSubmissionId] = useState
();
@@ -61,9 +62,11 @@ export default function AssignmentSubmission(props: Props) {
}, [endpointStatus, submissionId]);
useEffect(() => {
- if (endpointStatus) {
- setDirty(true);
+ if (!hasLoadedRef.current) {
+ hasLoadedRef.current = true;
+ return;
}
+ setDirty(true);
}, [submissionTextEditor, submissionLink]);
const loadSubmission = () => {
@@ -95,8 +98,6 @@ export default function AssignmentSubmission(props: Props) {
data.submission.recorded_score || data.submission.calculated_score
);
setSubmissionTextEditor(data.submission.sub_text || "");
- })
- .then(response => {
setDirty(false);
})
.finally(() => {
diff --git a/app/javascript/components/checkin/InstallmentReport.tsx b/app/javascript/components/checkin/InstallmentReport.tsx
index a181080a9..d88b4b9da 100644
--- a/app/javascript/components/checkin/InstallmentReport.tsx
+++ b/app/javascript/components/checkin/InstallmentReport.tsx
@@ -1,4 +1,4 @@
-import React, { Suspense, useState, useEffect } from "react";
+import React, { Suspense, useState, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router";
import { Accordion, AccordionTab } from "primereact/accordion";
@@ -80,6 +80,7 @@ export default function InstallmentReport(props: Props) {
const [contributions, setContributions] = useState({});
const [installment, setInstallment] = useState({ comments: "" });
const [dirty, setDirty] = useState(false);
+ const hasLoadedRef = useRef(false);
useDirtyStatus(category, dirty);
const [redirectState, setRedirectState] = useState(RedirectState.DECIDING);
@@ -100,7 +101,13 @@ export default function InstallmentReport(props: Props) {
setInstallment(inst);
};
- useEffect(() => setDirty(true), [contributions, installment]);
+ useEffect(() => {
+ if (!hasLoadedRef.current) {
+ hasLoadedRef.current = true;
+ return;
+ }
+ setDirty(true);
+ }, [contributions, installment]);
useEffect(() => {
if (endpointStatus) {
diff --git a/app/javascript/components/infrastructure/StatusSlice.ts b/app/javascript/components/infrastructure/StatusSlice.ts
index 7945e4002..2393fd9cc 100644
--- a/app/javascript/components/infrastructure/StatusSlice.ts
+++ b/app/javascript/components/infrastructure/StatusSlice.ts
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
import { useDispatch } from "react-redux";
import { createSlice } from "@reduxjs/toolkit";
@@ -91,14 +91,29 @@ export const {
export function useDirtyStatus(flagKey: string, dirty: boolean) {
const dispatch = useDispatch();
+ const hasInitialized = useRef(false);
+ const previousDirty = useRef(false);
useEffect(() => {
- if (dirty) {
- dispatch(setDirty(flagKey));
+ if (!hasInitialized.current) {
+ hasInitialized.current = true;
+ previousDirty.current = dirty;
+ if (dirty) {
+ dispatch(setClean(flagKey));
+ }
return;
}
- dispatch(setClean(flagKey));
+ if (dirty === previousDirty.current) {
+ return;
+ }
+
+ if (dirty) {
+ dispatch(setDirty(flagKey));
+ } else {
+ dispatch(setClean(flagKey));
+ }
+ previousDirty.current = dirty;
}, [dirty, flagKey, dispatch]);
}
From 3809eacadad1b2d3f873db6a1cb31ce204d7505b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Aug 2026 03:21:11 +0000
Subject: [PATCH 5/8] Apply remaining changes
Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com>
---
.../components/BingoBoards/BingoGameDataAdmin.tsx | 7 ++++---
.../components/BingoBoards/CandidateListEntry.tsx | 7 ++++---
.../components/assignments/AssignmentDataAdmin.tsx | 7 ++++---
.../components/assignments/AssignmentSubmission.tsx | 7 ++++---
app/javascript/components/checkin/InstallmentReport.tsx | 7 ++++---
app/javascript/components/infrastructure/StatusSlice.ts | 3 ---
6 files changed, 20 insertions(+), 18 deletions(-)
diff --git a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
index 11deaa01d..0d087e665 100644
--- a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
+++ b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
@@ -42,7 +42,7 @@ export default function BingoGameDataAdmin(props) {
const { t, i18n } = useTranslation(`${category}s`);
const [dirty, setDirty] = useState(false);
- const hasLoadedRef = useRef(false);
+ const isLoadingRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [messages, setMessages] = useState({});
@@ -87,8 +87,8 @@ export default function BingoGameDataAdmin(props) {
}, [endpointStatus]);
useEffect(() => {
- if (!hasLoadedRef.current) {
- hasLoadedRef.current = true;
+ if (isLoadingRef.current) {
+ isLoadingRef.current = false;
return;
}
setDirty(true);
@@ -207,6 +207,7 @@ export default function BingoGameDataAdmin(props) {
};
const getBingoGameData = () => {
+ isLoadingRef.current = true;
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === bingoGameId) {
diff --git a/app/javascript/components/BingoBoards/CandidateListEntry.tsx b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
index e75e46158..7a0cd39ea 100644
--- a/app/javascript/components/BingoBoards/CandidateListEntry.tsx
+++ b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
@@ -40,7 +40,7 @@ export default function CandidateListEntry(props: Props) {
const { bingoGameId } = useParams();
const [dirty, setDirty] = useState(false);
- const hasLoadedRef = useRef(false);
+ const isLoadingRef = useRef(false);
const dispatch = useDispatch();
useDirtyStatus(category, dirty);
@@ -59,6 +59,7 @@ export default function CandidateListEntry(props: Props) {
const [requestCollaborationUrl, setRequestCollaborationUrl] = useState("");
const getCandidateList = () => {
+ isLoadingRef.current = true;
dispatch(startTask());
const url =
props.rootPath === undefined
@@ -184,8 +185,8 @@ export default function CandidateListEntry(props: Props) {
}, [endpointStatus]);
useEffect(() => {
- if (!hasLoadedRef.current) {
- hasLoadedRef.current = true;
+ if (isLoadingRef.current) {
+ isLoadingRef.current = false;
return;
}
setDirty(true);
diff --git a/app/javascript/components/assignments/AssignmentDataAdmin.tsx b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
index 573e45ad6..840b70628 100644
--- a/app/javascript/components/assignments/AssignmentDataAdmin.tsx
+++ b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
@@ -44,7 +44,7 @@ export default function AssignmentDataAdmin(props) {
const navigate = useNavigate();
const [dirty, setDirty] = useState(false);
- const hasLoadedRef = useRef(false);
+ const isLoadingRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [assignmentProjects, setAssignmentProjects] = useState([
@@ -100,8 +100,8 @@ export default function AssignmentDataAdmin(props) {
}, [endpointStatus]);
useEffect(() => {
- if (!hasLoadedRef.current) {
- hasLoadedRef.current = true;
+ if (isLoadingRef.current) {
+ isLoadingRef.current = false;
return;
}
setDirty(true);
@@ -218,6 +218,7 @@ export default function AssignmentDataAdmin(props) {
setAssignmentRubricId(assignment.rubric_id || -1);
};
const getAssignmentData = () => {
+ isLoadingRef.current = true;
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === assignmentId) {
diff --git a/app/javascript/components/assignments/AssignmentSubmission.tsx b/app/javascript/components/assignments/AssignmentSubmission.tsx
index 7f635e5c7..868914c58 100644
--- a/app/javascript/components/assignments/AssignmentSubmission.tsx
+++ b/app/javascript/components/assignments/AssignmentSubmission.tsx
@@ -43,7 +43,7 @@ export default function AssignmentSubmission(props: Props) {
const navigate = useNavigate();
const [t, i18n] = useTranslation(`${category}s`);
const [dirty, setDirty] = useState(false);
- const hasLoadedRef = useRef(false);
+ const isLoadingRef = useRef(false);
useDirtyStatus(category, dirty);
const [submissionId, setSubmissionId] = useState();
@@ -62,14 +62,15 @@ export default function AssignmentSubmission(props: Props) {
}, [endpointStatus, submissionId]);
useEffect(() => {
- if (!hasLoadedRef.current) {
- hasLoadedRef.current = true;
+ if (isLoadingRef.current) {
+ isLoadingRef.current = false;
return;
}
setDirty(true);
}, [submissionTextEditor, submissionLink]);
const loadSubmission = () => {
+ isLoadingRef.current = true;
const url = props.rootPath === undefined
? `${endpoints.submissionUrl}${submissionId}.json`
: `/${props.rootPath}${endpoints.submissionUrl}${submissionId}.json`;
diff --git a/app/javascript/components/checkin/InstallmentReport.tsx b/app/javascript/components/checkin/InstallmentReport.tsx
index d88b4b9da..1b5e7b554 100644
--- a/app/javascript/components/checkin/InstallmentReport.tsx
+++ b/app/javascript/components/checkin/InstallmentReport.tsx
@@ -80,7 +80,7 @@ export default function InstallmentReport(props: Props) {
const [contributions, setContributions] = useState({});
const [installment, setInstallment] = useState({ comments: "" });
const [dirty, setDirty] = useState(false);
- const hasLoadedRef = useRef(false);
+ const isLoadingRef = useRef(false);
useDirtyStatus(category, dirty);
const [redirectState, setRedirectState] = useState(RedirectState.DECIDING);
@@ -102,8 +102,8 @@ export default function InstallmentReport(props: Props) {
};
useEffect(() => {
- if (!hasLoadedRef.current) {
- hasLoadedRef.current = true;
+ if (isLoadingRef.current) {
+ isLoadingRef.current = false;
return;
}
setDirty(true);
@@ -134,6 +134,7 @@ export default function InstallmentReport(props: Props) {
//Retrieve the latest data
const getContributions = () => {
+ isLoadingRef.current = true;
const url =
props.rootPath === undefined
? `${endpoints.baseUrl}${projectId}.json`
diff --git a/app/javascript/components/infrastructure/StatusSlice.ts b/app/javascript/components/infrastructure/StatusSlice.ts
index 2393fd9cc..d377fc8aa 100644
--- a/app/javascript/components/infrastructure/StatusSlice.ts
+++ b/app/javascript/components/infrastructure/StatusSlice.ts
@@ -98,9 +98,6 @@ export function useDirtyStatus(flagKey: string, dirty: boolean) {
if (!hasInitialized.current) {
hasInitialized.current = true;
previousDirty.current = dirty;
- if (dirty) {
- dispatch(setClean(flagKey));
- }
return;
}
From a5007f6ad4b869ae09466df6ee3460fa97fbf13b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:07:02 +0000
Subject: [PATCH 6/8] Tighten fresh-load dirty suppression for data-entry
screens
Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com>
---
app/javascript/components/AppStatusBar.tsx | 45 ++++++++++---------
.../BingoBoards/BingoGameDataAdmin.tsx | 11 +++--
.../BingoBoards/CandidateListEntry.tsx | 11 +++--
.../assignments/AssignmentDataAdmin.tsx | 11 +++--
.../assignments/AssignmentSubmission.tsx | 11 +++--
.../components/checkin/InstallmentReport.tsx | 11 +++--
6 files changed, 58 insertions(+), 42 deletions(-)
diff --git a/app/javascript/components/AppStatusBar.tsx b/app/javascript/components/AppStatusBar.tsx
index 0476a2ca4..0fc8e8ea6 100644
--- a/app/javascript/components/AppStatusBar.tsx
+++ b/app/javascript/components/AppStatusBar.tsx
@@ -47,28 +47,29 @@ export default function AppStatusBar() {
return (
<>
-
-
-
{hasDirtyChanges ? "Unsaved changes" : "Saved"}
+
+
+
+ {hasDirtyChanges ? "Unsaved changes" : "Saved"}
+
>
);
diff --git a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
index 0d087e665..e25aded90 100644
--- a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
+++ b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
@@ -42,7 +42,7 @@ export default function BingoGameDataAdmin(props) {
const { t, i18n } = useTranslation(`${category}s`);
const [dirty, setDirty] = useState(false);
- const isLoadingRef = useRef(false);
+ const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [messages, setMessages] = useState({});
@@ -87,8 +87,8 @@ export default function BingoGameDataAdmin(props) {
}, [endpointStatus]);
useEffect(() => {
- if (isLoadingRef.current) {
- isLoadingRef.current = false;
+ if (suppressDirtyRef.current) {
+ suppressDirtyRef.current = false;
return;
}
setDirty(true);
@@ -207,7 +207,10 @@ export default function BingoGameDataAdmin(props) {
};
const getBingoGameData = () => {
- isLoadingRef.current = true;
+ suppressDirtyRef.current = true;
+ setTimeout(() => {
+ suppressDirtyRef.current = false;
+ }, 0);
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === bingoGameId) {
diff --git a/app/javascript/components/BingoBoards/CandidateListEntry.tsx b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
index 7a0cd39ea..5d7914ed1 100644
--- a/app/javascript/components/BingoBoards/CandidateListEntry.tsx
+++ b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
@@ -40,7 +40,7 @@ export default function CandidateListEntry(props: Props) {
const { bingoGameId } = useParams();
const [dirty, setDirty] = useState(false);
- const isLoadingRef = useRef(false);
+ const suppressDirtyRef = useRef(false);
const dispatch = useDispatch();
useDirtyStatus(category, dirty);
@@ -59,7 +59,10 @@ export default function CandidateListEntry(props: Props) {
const [requestCollaborationUrl, setRequestCollaborationUrl] = useState("");
const getCandidateList = () => {
- isLoadingRef.current = true;
+ suppressDirtyRef.current = true;
+ setTimeout(() => {
+ suppressDirtyRef.current = false;
+ }, 0);
dispatch(startTask());
const url =
props.rootPath === undefined
@@ -185,8 +188,8 @@ export default function CandidateListEntry(props: Props) {
}, [endpointStatus]);
useEffect(() => {
- if (isLoadingRef.current) {
- isLoadingRef.current = false;
+ if (suppressDirtyRef.current) {
+ suppressDirtyRef.current = false;
return;
}
setDirty(true);
diff --git a/app/javascript/components/assignments/AssignmentDataAdmin.tsx b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
index 840b70628..de59bcc40 100644
--- a/app/javascript/components/assignments/AssignmentDataAdmin.tsx
+++ b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
@@ -44,7 +44,7 @@ export default function AssignmentDataAdmin(props) {
const navigate = useNavigate();
const [dirty, setDirty] = useState(false);
- const isLoadingRef = useRef(false);
+ const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [assignmentProjects, setAssignmentProjects] = useState([
@@ -100,8 +100,8 @@ export default function AssignmentDataAdmin(props) {
}, [endpointStatus]);
useEffect(() => {
- if (isLoadingRef.current) {
- isLoadingRef.current = false;
+ if (suppressDirtyRef.current) {
+ suppressDirtyRef.current = false;
return;
}
setDirty(true);
@@ -218,7 +218,10 @@ export default function AssignmentDataAdmin(props) {
setAssignmentRubricId(assignment.rubric_id || -1);
};
const getAssignmentData = () => {
- isLoadingRef.current = true;
+ suppressDirtyRef.current = true;
+ setTimeout(() => {
+ suppressDirtyRef.current = false;
+ }, 0);
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === assignmentId) {
diff --git a/app/javascript/components/assignments/AssignmentSubmission.tsx b/app/javascript/components/assignments/AssignmentSubmission.tsx
index 868914c58..35be9ad38 100644
--- a/app/javascript/components/assignments/AssignmentSubmission.tsx
+++ b/app/javascript/components/assignments/AssignmentSubmission.tsx
@@ -43,7 +43,7 @@ export default function AssignmentSubmission(props: Props) {
const navigate = useNavigate();
const [t, i18n] = useTranslation(`${category}s`);
const [dirty, setDirty] = useState(false);
- const isLoadingRef = useRef(false);
+ const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);
const [submissionId, setSubmissionId] = useState
();
@@ -62,15 +62,18 @@ export default function AssignmentSubmission(props: Props) {
}, [endpointStatus, submissionId]);
useEffect(() => {
- if (isLoadingRef.current) {
- isLoadingRef.current = false;
+ if (suppressDirtyRef.current) {
+ suppressDirtyRef.current = false;
return;
}
setDirty(true);
}, [submissionTextEditor, submissionLink]);
const loadSubmission = () => {
- isLoadingRef.current = true;
+ suppressDirtyRef.current = true;
+ setTimeout(() => {
+ suppressDirtyRef.current = false;
+ }, 0);
const url = props.rootPath === undefined
? `${endpoints.submissionUrl}${submissionId}.json`
: `/${props.rootPath}${endpoints.submissionUrl}${submissionId}.json`;
diff --git a/app/javascript/components/checkin/InstallmentReport.tsx b/app/javascript/components/checkin/InstallmentReport.tsx
index 1b5e7b554..aedb1a593 100644
--- a/app/javascript/components/checkin/InstallmentReport.tsx
+++ b/app/javascript/components/checkin/InstallmentReport.tsx
@@ -80,7 +80,7 @@ export default function InstallmentReport(props: Props) {
const [contributions, setContributions] = useState({});
const [installment, setInstallment] = useState({ comments: "" });
const [dirty, setDirty] = useState(false);
- const isLoadingRef = useRef(false);
+ const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);
const [redirectState, setRedirectState] = useState(RedirectState.DECIDING);
@@ -102,8 +102,8 @@ export default function InstallmentReport(props: Props) {
};
useEffect(() => {
- if (isLoadingRef.current) {
- isLoadingRef.current = false;
+ if (suppressDirtyRef.current) {
+ suppressDirtyRef.current = false;
return;
}
setDirty(true);
@@ -134,7 +134,10 @@ export default function InstallmentReport(props: Props) {
//Retrieve the latest data
const getContributions = () => {
- isLoadingRef.current = true;
+ suppressDirtyRef.current = true;
+ setTimeout(() => {
+ suppressDirtyRef.current = false;
+ }, 0);
const url =
props.rootPath === undefined
? `${endpoints.baseUrl}${projectId}.json`
From c02d3e6a1d5aabd314d4762c381ce473225f465e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:08:12 +0000
Subject: [PATCH 7/8] Remove premature dirty suppression reset and keep
hydration guard in the effect path
Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com>
---
app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx | 3 ---
app/javascript/components/BingoBoards/CandidateListEntry.tsx | 3 ---
app/javascript/components/assignments/AssignmentDataAdmin.tsx | 3 ---
app/javascript/components/assignments/AssignmentSubmission.tsx | 3 ---
app/javascript/components/checkin/InstallmentReport.tsx | 3 ---
5 files changed, 15 deletions(-)
diff --git a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
index e25aded90..62daabdb2 100644
--- a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
+++ b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
@@ -208,9 +208,6 @@ export default function BingoGameDataAdmin(props) {
const getBingoGameData = () => {
suppressDirtyRef.current = true;
- setTimeout(() => {
- suppressDirtyRef.current = false;
- }, 0);
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === bingoGameId) {
diff --git a/app/javascript/components/BingoBoards/CandidateListEntry.tsx b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
index 5d7914ed1..c077fd8c0 100644
--- a/app/javascript/components/BingoBoards/CandidateListEntry.tsx
+++ b/app/javascript/components/BingoBoards/CandidateListEntry.tsx
@@ -60,9 +60,6 @@ export default function CandidateListEntry(props: Props) {
const getCandidateList = () => {
suppressDirtyRef.current = true;
- setTimeout(() => {
- suppressDirtyRef.current = false;
- }, 0);
dispatch(startTask());
const url =
props.rootPath === undefined
diff --git a/app/javascript/components/assignments/AssignmentDataAdmin.tsx b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
index de59bcc40..56c11312e 100644
--- a/app/javascript/components/assignments/AssignmentDataAdmin.tsx
+++ b/app/javascript/components/assignments/AssignmentDataAdmin.tsx
@@ -219,9 +219,6 @@ export default function AssignmentDataAdmin(props) {
};
const getAssignmentData = () => {
suppressDirtyRef.current = true;
- setTimeout(() => {
- suppressDirtyRef.current = false;
- }, 0);
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === assignmentId) {
diff --git a/app/javascript/components/assignments/AssignmentSubmission.tsx b/app/javascript/components/assignments/AssignmentSubmission.tsx
index 35be9ad38..be39f53c4 100644
--- a/app/javascript/components/assignments/AssignmentSubmission.tsx
+++ b/app/javascript/components/assignments/AssignmentSubmission.tsx
@@ -71,9 +71,6 @@ export default function AssignmentSubmission(props: Props) {
const loadSubmission = () => {
suppressDirtyRef.current = true;
- setTimeout(() => {
- suppressDirtyRef.current = false;
- }, 0);
const url = props.rootPath === undefined
? `${endpoints.submissionUrl}${submissionId}.json`
: `/${props.rootPath}${endpoints.submissionUrl}${submissionId}.json`;
diff --git a/app/javascript/components/checkin/InstallmentReport.tsx b/app/javascript/components/checkin/InstallmentReport.tsx
index aedb1a593..bcc84658a 100644
--- a/app/javascript/components/checkin/InstallmentReport.tsx
+++ b/app/javascript/components/checkin/InstallmentReport.tsx
@@ -135,9 +135,6 @@ export default function InstallmentReport(props: Props) {
//Retrieve the latest data
const getContributions = () => {
suppressDirtyRef.current = true;
- setTimeout(() => {
- suppressDirtyRef.current = false;
- }, 0);
const url =
props.rootPath === undefined
? `${endpoints.baseUrl}${projectId}.json`
From ee3e66f78199d71bddecc57f175c812cb3d8faa9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:29:27 +0000
Subject: [PATCH 8/8] Fix stale dirty state and status bar layering
Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com>
---
app/javascript/components/AppStatusBar.tsx | 6 ++++--
.../components/infrastructure/StatusSlice.ts | 13 ++++++++++++-
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/app/javascript/components/AppStatusBar.tsx b/app/javascript/components/AppStatusBar.tsx
index 0fc8e8ea6..96d6bbfef 100644
--- a/app/javascript/components/AppStatusBar.tsx
+++ b/app/javascript/components/AppStatusBar.tsx
@@ -47,7 +47,7 @@ export default function AppStatusBar() {
return (
<>
-
+
diff --git a/app/javascript/components/infrastructure/StatusSlice.ts b/app/javascript/components/infrastructure/StatusSlice.ts
index d377fc8aa..93560583a 100644
--- a/app/javascript/components/infrastructure/StatusSlice.ts
+++ b/app/javascript/components/infrastructure/StatusSlice.ts
@@ -92,12 +92,17 @@ export const {
export function useDirtyStatus(flagKey: string, dirty: boolean) {
const dispatch = useDispatch();
const hasInitialized = useRef(false);
- const previousDirty = useRef(false);
+ const previousDirty = useRef(dirty);
useEffect(() => {
if (!hasInitialized.current) {
hasInitialized.current = true;
previousDirty.current = dirty;
+ if (dirty) {
+ dispatch(setDirty(flagKey));
+ } else {
+ dispatch(setClean(flagKey));
+ }
return;
}
@@ -112,6 +117,12 @@ export function useDirtyStatus(flagKey: string, dirty: boolean) {
}
previousDirty.current = dirty;
}, [dirty, flagKey, dispatch]);
+
+ useEffect(() => {
+ return () => {
+ dispatch(setClean(flagKey));
+ };
+ }, [dispatch, flagKey]);
}
export default reducer;