From a36cd13c080e05853913f28c1688a7f47e928f88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:38:00 +0000 Subject: [PATCH 01/28] Initial plan From 7c6efa0b21db7b0fd57205bc88dba58a007c1fb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:41:15 +0000 Subject: [PATCH 02/28] Fix assignment completion feedback and redirect UX Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- .../assignments/AssignmentSubmission.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/javascript/components/assignments/AssignmentSubmission.tsx b/app/javascript/components/assignments/AssignmentSubmission.tsx index 19cd77c80..e1a29d804 100644 --- a/app/javascript/components/assignments/AssignmentSubmission.tsx +++ b/app/javascript/components/assignments/AssignmentSubmission.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useMemo } from "react"; +import { useNavigate } from "react-router"; import { Temporal, TemporalSettings as Settings, parseISO } from "../infrastructure/TemporalSettings"; //Redux store stuff import { useDispatch } from "react-redux"; -import { startTask, endTask } from "../infrastructure/StatusSlice"; +import { startTask, endTask, addMessage, Priorities } from "../infrastructure/StatusSlice"; import { IAssignment } from "./AssignmentViewer"; import { useTypedSelector } from "../infrastructure/AppReducers"; @@ -39,6 +40,7 @@ export default function AssignmentSubmission(props: Props) { ); const dispatch = useDispatch(); + const navigate = useNavigate(); const [t, i18n] = useTranslation(`${category}s`); const [dirty, setDirty] = useState(false); @@ -112,7 +114,7 @@ export default function AssignmentSubmission(props: Props) { value={submissionTextEditor} headerTemplate={} onTextChange={e => { - setSubmissionTextEditor(e.htmlValue); + setSubmissionTextEditor(e.htmlValue || ""); }} /> @@ -166,6 +168,12 @@ export default function AssignmentSubmission(props: Props) { }) .then(response => { const data = response.data; + const successMessage = data?.messages?.main; + + if (successMessage) { + dispatch(addMessage(successMessage, new Date(), Priorities.INFO)); + } + if (data.messages !== null && Object.keys(data.messages).length < 2) { setSubmissionId(data.submission.id); let receivedDate = parseISO(data.submission.updated_at, Settings.timezone); @@ -179,7 +187,12 @@ export default function AssignmentSubmission(props: Props) { setWithdrawnDate(receivedDate); } setRecordedScore(data.submission.recorded_score); - setSubmissionTextEditor(data.submission.sub_text); + setSubmissionTextEditor(data.submission.sub_text || ""); + setDirty(false); + + if (submitIt) { + navigate("/home"); + } } }) .then(props.reloadCallback) From f1e759d72b4ab6fa46d6774625a8e7e828a261e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:56:03 +0000 Subject: [PATCH 03/28] Apply remaining changes Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- app/javascript/components/AppStatusBar.tsx | 26 ++++++++++++++++++- .../BingoBoards/BingoGameDataAdmin.tsx | 3 ++- .../BingoBoards/CandidateListEntry.tsx | 4 ++- app/javascript/components/ConceptsTable.tsx | 3 ++- .../assignments/AssignmentDataAdmin.tsx | 3 ++- .../assignments/AssignmentSubmission.tsx | 3 ++- .../components/checkin/InstallmentReport.tsx | 4 ++- .../components/infrastructure/StatusSlice.ts | 16 ++++++++++++ 8 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app/javascript/components/AppStatusBar.tsx b/app/javascript/components/AppStatusBar.tsx index 4841c46e6..bef6f597f 100644 --- a/app/javascript/components/AppStatusBar.tsx +++ b/app/javascript/components/AppStatusBar.tsx @@ -10,6 +10,9 @@ export default function AppStatusBar(props) { const messages = useTypedSelector(state => { return state.status.messages; }); + const hasDirtyChanges = useTypedSelector(state => { + return Object.values(state.status.dirtyStatus).some(Boolean); + }); const dispatch = useDispatch(); const toast = React.useRef(null); @@ -27,5 +30,26 @@ export default function AppStatusBar(props) { }); }, [messages]); - return ; + return ( + <> + +
+ + {hasDirtyChanges ? "Unsaved changes" : "Saved"} +
+ + ); } diff --git a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx index df371b99f..83fed88e3 100644 --- a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx +++ b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx @@ -10,7 +10,7 @@ import { Button } from "primereact/button"; import { useTranslation } from "react-i18next"; import { useTypedSelector } from "../infrastructure/AppReducers"; -import { startTask, endTask } from "../infrastructure/StatusSlice"; +import { startTask, endTask, useDirtyStatus } from "../infrastructure/StatusSlice"; import axios from "axios"; import { Editor } from "primereact/editor"; import EditorToolbar from "../toolbars/EditorToolbar"; @@ -42,6 +42,7 @@ export default function BingoGameDataAdmin(props) { const { t, i18n } = useTranslation(`${category}s`); const [dirty, setDirty] = useState(false); + useDirtyStatus(category, dirty); const [curTab, setCurTab] = useState(0); const [messages, setMessages] = useState({}); const [gameProjects, setGameProjects] = useState([ diff --git a/app/javascript/components/BingoBoards/CandidateListEntry.tsx b/app/javascript/components/BingoBoards/CandidateListEntry.tsx index 5bd70f652..7b4d49f1d 100644 --- a/app/javascript/components/BingoBoards/CandidateListEntry.tsx +++ b/app/javascript/components/BingoBoards/CandidateListEntry.tsx @@ -10,7 +10,8 @@ import { startTask, endTask, addMessage, - Priorities + Priorities, + useDirtyStatus } from "../infrastructure/StatusSlice"; import { useTypedSelector } from "../infrastructure/AppReducers"; import axios from "axios"; @@ -40,6 +41,7 @@ export default function CandidateListEntry(props: Props) { const [dirty, setDirty] = useState(false); const dispatch = useDispatch(); + useDirtyStatus(category, dirty); const [candidateListId, setCandidateListId] = useState(0); const [topic, setTopic] = useState(""); diff --git a/app/javascript/components/ConceptsTable.tsx b/app/javascript/components/ConceptsTable.tsx index bbf099526..2c31e4357 100644 --- a/app/javascript/components/ConceptsTable.tsx +++ b/app/javascript/components/ConceptsTable.tsx @@ -15,7 +15,7 @@ import { Column } from "primereact/column"; import { Dialog } from "primereact/dialog"; import { useDispatch } from "react-redux"; -import { startTask, endTask, addMessage, Priorities } from "./infrastructure/StatusSlice"; +import { startTask, endTask, addMessage, Priorities, useDirtyStatus } from "./infrastructure/StatusSlice"; import { InputText } from "primereact/inputtext"; enum OPT_COLS { @@ -52,6 +52,7 @@ export default function ConceptsTable() { const [editing, setEditing] = useState(false); const [dirty, setDirty] = useState(false); + useDirtyStatus(category, dirty); const [conceptName, setConceptName] = useState(""); const [conceptId, setConceptId] = useState(-1); diff --git a/app/javascript/components/assignments/AssignmentDataAdmin.tsx b/app/javascript/components/assignments/AssignmentDataAdmin.tsx index e60c07703..b856261f0 100644 --- a/app/javascript/components/assignments/AssignmentDataAdmin.tsx +++ b/app/javascript/components/assignments/AssignmentDataAdmin.tsx @@ -20,7 +20,7 @@ import { useTranslation } from "react-i18next"; import EditorToolbar from "../toolbars/EditorToolbar"; import { useTypedSelector } from "../infrastructure/AppReducers"; -import { startTask, endTask, addMessage, Priorities } from "../infrastructure/StatusSlice"; +import { startTask, endTask, addMessage, Priorities, useDirtyStatus } from "../infrastructure/StatusSlice"; import { Col, Container, Row } from "react-grid-system"; import { utcAdjustDate, utcAdjustEndDate } from "../infrastructure/Utilities"; import { FloatLabel } from "primereact/floatlabel"; @@ -44,6 +44,7 @@ export default function AssignmentDataAdmin(props) { const navigate = useNavigate(); const [dirty, setDirty] = useState(false); + useDirtyStatus(category, dirty); const [curTab, setCurTab] = useState(0); const [assignmentProjects, setAssignmentProjects] = useState([ { id: -1, name: "None Selected" } diff --git a/app/javascript/components/assignments/AssignmentSubmission.tsx b/app/javascript/components/assignments/AssignmentSubmission.tsx index e1a29d804..6bf0cb5aa 100644 --- a/app/javascript/components/assignments/AssignmentSubmission.tsx +++ b/app/javascript/components/assignments/AssignmentSubmission.tsx @@ -4,7 +4,7 @@ import { Temporal, TemporalSettings as Settings, parseISO } from "../infrastruct //Redux store stuff import { useDispatch } from "react-redux"; -import { startTask, endTask, addMessage, Priorities } from "../infrastructure/StatusSlice"; +import { startTask, endTask, addMessage, Priorities, useDirtyStatus } from "../infrastructure/StatusSlice"; import { IAssignment } from "./AssignmentViewer"; import { useTypedSelector } from "../infrastructure/AppReducers"; @@ -43,6 +43,7 @@ export default function AssignmentSubmission(props: Props) { const navigate = useNavigate(); const [t, i18n] = useTranslation(`${category}s`); const [dirty, setDirty] = useState(false); + useDirtyStatus(category, dirty); const [submissionId, setSubmissionId] = useState(); const [updatedDate, setUpdatedDate] = useState(null); diff --git a/app/javascript/components/checkin/InstallmentReport.tsx b/app/javascript/components/checkin/InstallmentReport.tsx index 709cafdc1..a181080a9 100644 --- a/app/javascript/components/checkin/InstallmentReport.tsx +++ b/app/javascript/components/checkin/InstallmentReport.tsx @@ -10,7 +10,8 @@ import { startTask, endTask, addMessage, - Priorities + Priorities, + useDirtyStatus } from "../infrastructure/StatusSlice"; import { useTranslation } from "react-i18next"; import { useTypedSelector } from "../infrastructure/AppReducers"; @@ -79,6 +80,7 @@ export default function InstallmentReport(props: Props) { const [contributions, setContributions] = useState({}); const [installment, setInstallment] = useState({ comments: "" }); const [dirty, setDirty] = useState(false); + useDirtyStatus(category, dirty); const [redirectState, setRedirectState] = useState(RedirectState.DECIDING); const [redirectUrl, setRedirectUrl] = useState(undefined); diff --git a/app/javascript/components/infrastructure/StatusSlice.ts b/app/javascript/components/infrastructure/StatusSlice.ts index 9f15839ba..7945e4002 100644 --- a/app/javascript/components/infrastructure/StatusSlice.ts +++ b/app/javascript/components/infrastructure/StatusSlice.ts @@ -1,3 +1,5 @@ +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; import { createSlice } from "@reduxjs/toolkit"; export enum Priorities { @@ -86,4 +88,18 @@ export const { addMessage, acknowledgeMsg } = actions; + +export function useDirtyStatus(flagKey: string, dirty: boolean) { + const dispatch = useDispatch(); + + useEffect(() => { + if (dirty) { + dispatch(setDirty(flagKey)); + return; + } + + dispatch(setClean(flagKey)); + }, [dirty, flagKey, dispatch]); +} + export default reducer; From 1965a9c7b7539677d72724cba8bfe2335a075316 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:18:47 +0000 Subject: [PATCH 04/28] Fix stale dirty-state initialization in form loaders Co-authored-by: mgmodell <7279993+mgmodell@users.noreply.github.com> --- app/javascript/components/AppStatusBar.tsx | 52 +++++++++++++------ .../BingoBoards/BingoGameDataAdmin.tsx | 9 +++- .../BingoBoards/CandidateListEntry.tsx | 8 ++- .../assignments/AssignmentDataAdmin.tsx | 8 ++- .../assignments/AssignmentSubmission.tsx | 11 ++-- .../components/checkin/InstallmentReport.tsx | 11 +++- .../components/infrastructure/StatusSlice.ts | 23 ++++++-- 7 files changed, 89 insertions(+), 33 deletions(-) diff --git a/app/javascript/components/AppStatusBar.tsx b/app/javascript/components/AppStatusBar.tsx index bef6f597f..0476a2ca4 100644 --- a/app/javascript/components/AppStatusBar.tsx +++ b/app/javascript/components/AppStatusBar.tsx @@ -6,45 +6,65 @@ import { acknowledgeMsg } from "./infrastructure/StatusSlice"; import { Toast } from "primereact/toast"; -export default function AppStatusBar(props) { - const messages = useTypedSelector(state => { - return state.status.messages; +type AppMessage = { + text: string; + priority: "error" | "info" | "warning"; + dismissed: boolean; +}; + +export default function AppStatusBar() { + const messages = useTypedSelector((state): AppMessage[] => { + return state.status.messages ?? []; }); const hasDirtyChanges = useTypedSelector(state => { - return Object.values(state.status.dirtyStatus).some(Boolean); + const dirtyStatus = state.status.dirtyStatus as Record; + for (const key in dirtyStatus) { + if (dirtyStatus[key]) { + return true; + } + } + return false; }); const dispatch = useDispatch(); - const toast = React.useRef(null); + const toast = React.useRef(null); useEffect(() => { - messages.forEach((message, index) => { + messages.forEach((message: AppMessage, index: number) => { if (!message.dismissed) { - toast.current.show({ - severity: message.priority, - summary: message.priority, - detail: message.text, - life: 30000 - }); + if (toast.current) { + toast.current.show({ + severity: message.priority, + summary: message.priority, + detail: message.text, + life: 30000 + }); + } dispatch(acknowledgeMsg(index)); } }); - }, [messages]); + }, [dispatch, messages]); return ( <>
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 05/28] 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 06/28] 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 07/28] 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 08/28] 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; From 2f9eb59a3c99e959462ba584b0444c0a15c35d03 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Thu, 10 Sep 2026 18:41:08 +0000 Subject: [PATCH 09/28] in progress - need another dirty state --- Gemfile.lock | 10 +-- app/javascript/components/ActivityLib.tsx | 38 ++++----- app/javascript/components/AppStatusBar.tsx | 35 --------- app/javascript/components/PageWrapper.tsx | 2 +- .../components/checkin/InstallmentReport.tsx | 2 + .../infrastructure/DirtyIndicator.tsx | 52 +++++++++++++ .../components/toolbars/AppHeader.tsx | 8 +- .../components/toolbars/HelpMenu.tsx | 77 +++++++++++-------- app/javascript/css/application.css | 15 ++++ config/locales/home.en.yml | 1 + db/schema.rb | 2 +- mise.toml | 2 +- 12 files changed, 147 insertions(+), 97 deletions(-) create mode 100644 app/javascript/components/infrastructure/DirtyIndicator.tsx diff --git a/Gemfile.lock b/Gemfile.lock index 8f0c91fbd..96db9c581 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -376,8 +376,8 @@ GEM logger rack (>= 2.2.3) rack-protection - omniauth-google-oauth2 (1.2.2) - jwt (>= 2.9.2) + omniauth-google-oauth2 (1.2.3) + jwt (>= 2.9.2, < 4) oauth2 (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) @@ -592,7 +592,7 @@ GEM railties (>= 5.2) semantic_range (>= 2.3.0) simple_po_parser (1.1.6) - simpleidn (0.2.3) + simpleidn (0.3.0) snaky_hash (2.0.7) hashie (>= 0.1.0, < 6) version_gem (~> 1.1, >= 1.1.14) @@ -869,7 +869,7 @@ CHECKSUMS oauth2 (2.0.25) sha256=2f736a2f93c2caa67c1b08dc3c9889bb907d62643f3183fefaa1723cba6a82ac oj (3.17.6) sha256=f511257e01a12030c3ceb8023cd2e96fca924ea9c741f57ea366816f1fd8e68c omniauth (2.1.4) sha256=42a05b0496f0d22e1dd85d42aaf602f064e36bb47a6826a27ab55e5ba608763c - omniauth-google-oauth2 (1.2.2) sha256=74c3f3d0221c048f938846092fb15a1f15237526f50a7c93d9793f9a4ff1be11 + omniauth-google-oauth2 (1.2.3) sha256=bdbab67e64e50b7902eee2dcfc2225575cefba15ab89a3b6e92c8947987c1da9 omniauth-oauth2 (1.9.0) sha256=ed15f6d9d20991807ce114cc5b9c1453bce3645b64e51c68c90cff5ff153fee8 orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 @@ -944,7 +944,7 @@ CHECKSUMS sexp_processor (4.17.5) sha256=ae2b48ba98353d5d465ce8759836b7a05f2e12c5879fcd14d7815b026de32f0e shakapacker (10.3.2) sha256=cfc8fa328d39b5321912376faa13710bd1ac10bc3eab3c95e6d71eb28f9ba0e0 simple_po_parser (1.1.6) sha256=122687d44d3de516a0e69e2f383a4180f5015e8c5ed5a7f2258f2b376f64cbf3 - simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29 + simpleidn (0.3.0) sha256=12ca730bed2f3db04d11e9bfd1bca3e11fb37f55b21eb2e9793fb5814bf54d03 snaky_hash (2.0.7) sha256=7d02c70012a3f932e48860cd024577908300c9aa615e0cb9b450aaa749cbcb4d solid_cable (4.0.2) sha256=084636a67679ad00d23088b33c84047e614bcf41ee559db24b414d83cdc42d03 spring (4.7.0) sha256=f81bf6d41f05518ed5d0128ce5c75fb6dd9c6153f158129c8b85c4461ca0b72c diff --git a/app/javascript/components/ActivityLib.tsx b/app/javascript/components/ActivityLib.tsx index c672e1ac2..7706b4155 100644 --- a/app/javascript/components/ActivityLib.tsx +++ b/app/javascript/components/ActivityLib.tsx @@ -9,95 +9,95 @@ export function iconForType(type: string) { switch (type.toLowerCase()) { case "home": iconData.className = "home"; - iconData.icon = ; + iconData.icon = ; break; case "welcome": iconData.className = "welcome"; - iconData.icon = ; + iconData.icon = ; break; case "profile": iconData.className = "profile"; - iconData.icon = ; + iconData.icon = ; break; case "perspective": iconData.className = "perspective"; - iconData.icon = ; + iconData.icon = ; break; case "concept": case "concepts": iconData.className = "concept"; - iconData.icon = ; + iconData.icon = ; break; case "group experience": case "experience": case "experiences": iconData.className = "experience"; - iconData.icon = ; + iconData.icon = ; break; case "project": case "assessment": case "assessments": iconData.className = "assessment"; - iconData.icon = ; + iconData.icon = ; break; case "terms list": case "bingo_game": case "bingo games": iconData.className = "bingo_game"; - iconData.icon = ; + iconData.icon = ; break; case "group assignment": case "assignment": case "assignments": iconData.className = "assignment"; - iconData.icon = ; + iconData.icon = ; break; case "submission": iconData.className = "submission"; - iconData.icon = ; + iconData.icon = ; break; case "rubric": case "rubrics": iconData.className = "rubric"; - iconData.icon = ; + iconData.icon = ; break; case "course": case "courses": iconData.className = "course"; - iconData.icon = ; + iconData.icon = ; break; case "user": case "users": iconData.className = "user"; - iconData.icon = ; + iconData.icon = ; break; case "reporting": iconData.className = "reporting"; - iconData.icon = ; + iconData.icon = ; break; case "administration": case "admin": iconData.className = "admin"; - iconData.icon = ; + iconData.icon = ; break; case "school": case "schools": iconData.className = "school"; - iconData.icon = ; + iconData.icon = ; break; case "consent_form": case "consent_forms": iconData.className = "consent_form"; - iconData.icon = ; + iconData.icon = ; break; case 'demo': case 'demonstration': iconData.className = "demonstration"; - iconData.icon = ; + iconData.icon = ; break; default: iconData.className = `unit-${type}`; - iconData.icon = ; + iconData.icon = ; } return ( <> diff --git a/app/javascript/components/AppStatusBar.tsx b/app/javascript/components/AppStatusBar.tsx index 96d6bbfef..490cc0d40 100644 --- a/app/javascript/components/AppStatusBar.tsx +++ b/app/javascript/components/AppStatusBar.tsx @@ -16,15 +16,6 @@ export default function AppStatusBar() { const messages = useTypedSelector((state): AppMessage[] => { return state.status.messages ?? []; }); - const hasDirtyChanges = useTypedSelector(state => { - const dirtyStatus = state.status.dirtyStatus as Record; - for (const key in dirtyStatus) { - if (dirtyStatus[key]) { - return true; - } - } - return false; - }); const dispatch = useDispatch(); const toast = React.useRef(null); @@ -47,32 +38,6 @@ export default function AppStatusBar() { return ( <> -
-
- - {hasDirtyChanges ? "Unsaved changes" : "Saved"} -
-
); } diff --git a/app/javascript/components/PageWrapper.tsx b/app/javascript/components/PageWrapper.tsx index 20c7a682b..50f36d33d 100644 --- a/app/javascript/components/PageWrapper.tsx +++ b/app/javascript/components/PageWrapper.tsx @@ -62,7 +62,6 @@ export default function PageWrapper(props: Readonly) { element={ }> -
@@ -72,6 +71,7 @@ export default function PageWrapper(props: Readonly) { +
} diff --git a/app/javascript/components/checkin/InstallmentReport.tsx b/app/javascript/components/checkin/InstallmentReport.tsx index bcc84658a..ec0252634 100644 --- a/app/javascript/components/checkin/InstallmentReport.tsx +++ b/app/javascript/components/checkin/InstallmentReport.tsx @@ -202,6 +202,7 @@ export default function InstallmentReport(props: Props) { setDirty(false); setGroup(data.group); + suppressDirtyRef.current = true; setProject(data.installment.project); }) .catch(error => { @@ -249,6 +250,7 @@ export default function InstallmentReport(props: Props) { }, {} ); + suppressDirtyRef.current = true; setContributions(receivedContributions); navigate('/home'); } diff --git a/app/javascript/components/infrastructure/DirtyIndicator.tsx b/app/javascript/components/infrastructure/DirtyIndicator.tsx new file mode 100644 index 000000000..bcb444958 --- /dev/null +++ b/app/javascript/components/infrastructure/DirtyIndicator.tsx @@ -0,0 +1,52 @@ +import React, { useEffect } from "react"; +import { useTypedSelector } from "./AppReducers"; +import { useTranslation } from "react-i18next"; + +type Props = { +}; +export default function DirtyIndicator(props: Props) { + + const category = 'home'; + const {t} = useTranslation(category); + + const hasDirtyChanges = useTypedSelector(state => { + const dirtyStatus = state.status.dirtyStatus as Record; + for (const key in dirtyStatus) { + if (dirtyStatus[key]) { + return true; + } + } + return false; + }); + + return ( + hasDirtyChanges ? ( +
+
+ +
+
+ + ) : null + ) +} \ No newline at end of file diff --git a/app/javascript/components/toolbars/AppHeader.tsx b/app/javascript/components/toolbars/AppHeader.tsx index bf56e5ff5..ba77863ec 100644 --- a/app/javascript/components/toolbars/AppHeader.tsx +++ b/app/javascript/components/toolbars/AppHeader.tsx @@ -10,7 +10,11 @@ import { Toolbar } from "primereact/toolbar"; import { Skeleton } from "primereact/skeleton"; import AppBreadCrumb from "./AppBreadcrumb"; -export default function AppHeader(props) { +type Props = { + identifier?: string; +} + +export default function AppHeader(props: Props) { const endpointSet = "home"; const [t] = useTranslation( endpointSet); const endpoints = useTypedSelector( @@ -22,7 +26,7 @@ export default function AppHeader(props) { const working = useTypedSelector(state => { let accum = 0; - if (undefined === props.identifier) { + if (undefined !== props.identifier) { accum = state.status.tasks[props.identifier]; } else { accum = Number( diff --git a/app/javascript/components/toolbars/HelpMenu.tsx b/app/javascript/components/toolbars/HelpMenu.tsx index b1ec73ed5..8a1a68444 100644 --- a/app/javascript/components/toolbars/HelpMenu.tsx +++ b/app/javascript/components/toolbars/HelpMenu.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useLocation } from "react-router"; // Icons -import {driver } from "driver.js"; +import { driver } from "driver.js"; import "driver.js/dist/driver.css"; import { useTranslation } from "react-i18next"; @@ -11,6 +11,7 @@ import { useTour } from "../infrastructure/TourContext"; import { Button } from "primereact/button"; import { Sidebar } from "primereact/sidebar"; import LangButton from "./LangButton"; +import DirtyIndicator from "../infrastructure/DirtyIndicator"; type Props = { lookupUrl: string; @@ -75,44 +76,54 @@ export default function HelpMenu(props: Props) { const pathComponents = location.pathname.split("/"); return ( - - setShowInfo(false)} - > - {candidateFeedbackInfo()} - - -
+
); } diff --git a/app/javascript/css/application.css b/app/javascript/css/application.css index d378812d1..5e2f28baa 100644 --- a/app/javascript/css/application.css +++ b/app/javascript/css/application.css @@ -183,4 +183,19 @@ a:active { .p-breadcrumb a { padding: 0; color: var(--lighter-color); +} + +.help-grid { + display: flex; + flex-direction: row; + gap: 1px; +} + +.help-stacked { + display: flex; + flex-direction: column; + gap: 0; +} +.help-info { + grid-row: span 2; } \ No newline at end of file diff --git a/config/locales/home.en.yml b/config/locales/home.en.yml index d4e00e80d..a1763b783 100644 --- a/config/locales/home.en.yml +++ b/config/locales/home.en.yml @@ -234,6 +234,7 @@ en: and you cannot enroll in your own course. self_enroll: Enroll me! self_enroll_cancel: Cancel and Return Home + unsaved_changes: Unsaved changes list: type: Type task_name: Task Name diff --git a/db/schema.rb b/db/schema.rb index ffb59aa8f..b25000a35 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -632,7 +632,7 @@ t.index ["updated_at"], name: "index_sessions_on_updated_at" end - create_table "solid_cable_messages", charset: "utf8mb3", collation: "utf8mb3_uca1400_ai_ci", force: :cascade do |t| + create_table "solid_cable_messages", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.binary "channel", limit: 1024, null: false t.bigint "channel_hash", null: false t.datetime "created_at", null: false diff --git a/mise.toml b/mise.toml index b8bfff0ba..30c1d4b8b 100644 --- a/mise.toml +++ b/mise.toml @@ -5,5 +5,5 @@ podman = { version = "6.1.0", rename_exe = "podman" } python = "3.13.7" ruby = "4.0.6" rust = "1.97.1" -usage = "6.5.0" +usage = "6.6.0" yarn = "4.18.0" From 291598cb49e4405220e0f32837a62d881b61c685 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Fri, 11 Sep 2026 01:03:06 +0000 Subject: [PATCH 10/28] version updates --- Gemfile | 2 +- Gemfile.lock | 10 ++++---- package.json | 10 ++++---- yarn.lock | 67 +++++++++++++++++++++++++++++----------------------- 4 files changed, 49 insertions(+), 40 deletions(-) diff --git a/Gemfile b/Gemfile index a5e1b9627..833eef568 100644 --- a/Gemfile +++ b/Gemfile @@ -98,7 +98,7 @@ group :development, :test do gem 'report_builder' gem 'rspec' gem 'rails_best_practices' - gem 'rubocop', '~> 1.82', require: false + gem 'rubocop', '~> 1.91', require: false gem 'reek' gem 'rubocop-thread_safety' gem 'rubocop-performance' diff --git a/Gemfile.lock b/Gemfile.lock index 5ac0ec9df..44aa92849 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,7 +108,7 @@ GEM auth-sanitizer (0.2.3) version_gem (~> 1.1, >= 1.1.14) aws-eventstream (1.4.0) - aws-partitions (1.1284.0) + aws-partitions (1.1285.0) aws-sdk-core (3.255.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -536,7 +536,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.7) - rubocop (1.90.0) + rubocop (1.91.0) json (>= 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -712,7 +712,7 @@ DEPENDENCIES reek report_builder rspec - rubocop (~> 1.82) + rubocop (~> 1.91) rubocop-capybara rubocop-performance rubocop-rails @@ -753,7 +753,7 @@ CHECKSUMS ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 auth-sanitizer (0.2.3) sha256=db10aac92cfbe4c64ab637eebcbe1d67395d1694798041362173370f59933e3c aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b - aws-partitions (1.1284.0) sha256=026432da13da430a31ba7c30c0c210b35fa7d738399977f033d4a5a354de58dc + aws-partitions (1.1285.0) sha256=db3bdc741ecdd529d9a2e96c2a96ddf32e13e4337bcb10db060d87e93ece2485 aws-sdk-core (3.255.0) sha256=2bac7fbc8796e4e2eb8e6a6edebcb880d7023a922af97b15d2a8a26c9283343f aws-sdk-kms (1.131.0) sha256=b60d28045cd93c604142cb691b15c7ddc1e6738c4ee5a90db4f2b91f0ada1d15 aws-sdk-s3 (1.231.0) sha256=a9fc98c6f03f0e71c7215d48ff8844d436421df7f9486301d56bdf1f368a3364 @@ -930,7 +930,7 @@ CHECKSUMS rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c - rubocop (1.90.0) sha256=9eb4c065b5c5154e4ef554c547972f3905a9eb6b53e657e580b6796b54bf8242 + rubocop (1.91.0) sha256=9c82b7bf391c5d7e3798c5b9996e22a1fe3bd7468e351dfdeb96140c058296d0 rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db rubocop-capybara (3.0.0) sha256=7a64655238acda7f8f3c87e37ac825a64c615a79c17c253f1a28270dc3768c4b rubocop-performance (1.27.0) sha256=eeeb1374d062a368ee1c787b70eb0b0cc4b184cb1f8565f424760946146d61ce diff --git a/package.json b/package.json index e73accdcc..a7bbb706e 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "@rspack/dev-server": "^2.2.1", "@rspack/plugin-react-refresh": "^2.0.2", "@swc/core": "^1.16.2", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "@types/react-redux": "^7.1.34", "@types/stopword": "^2.0.3", "@visx/axis": "^4.0.0", @@ -53,10 +53,10 @@ "promise-polyfill": "^8.3.0", "prop-types": "^15.8.1", "quill": "^2.0.3", - "react": "^19.2.8", + "react": "^19.3.0", "react-big-calendar": "^1.20.0", "react-cookie-consent": "^10.0.2", - "react-dom": "^19.2.8", + "react-dom": "^19.3.0", "react-grid-system": "^8.2.1", "react-i18next": "^17.0.13", "react-on-rails": "17.0.1", @@ -83,7 +83,7 @@ "@types/d3-array": "^3.2.2", "@types/d3-time-format": "^4.0.3", "@types/react-big-calendar": "^1.16.3", - "react-refresh": "^0.18.0", + "react-refresh": "^0.19.0", "selfsigned": "^5.5.0" } } diff --git a/yarn.lock b/yarn.lock index 0876ded09..21a4c737b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -991,12 +991,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^19.2.7": - version: 19.2.7 - resolution: "@types/react-dom@npm:19.2.7" +"@types/react-dom@npm:^19.3.0": + version: 19.3.0 + resolution: "@types/react-dom@npm:19.3.0" peerDependencies: - "@types/react": ^19.2.0 - checksum: 10c0/226ab7e2b62114125e2276b3dcc3a25060177a673f8abadb7e31f395003de72ad68075c6edc5f51b02dd1e47443920ee030239c474c47fad075ded7410836909 + "@types/react": ^19.3.0 + checksum: 10c0/ce0066d28b62bc6afdd17ca84756b8fbe0f6ffc0c8b1e60739abc30e81510da2edbc1007e6ab3d974d30f5a8c25bb0f7de8d7ce09736c909fe84c0f9aed0b946 languageName: node linkType: hard @@ -1021,7 +1021,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:>=16.9.11, @types/react@npm:^19.2.18": +"@types/react@npm:*, @types/react@npm:>=16.9.11": version: 19.2.18 resolution: "@types/react@npm:19.2.18" dependencies: @@ -1030,6 +1030,15 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^19.3.0": + version: 19.3.0 + resolution: "@types/react@npm:19.3.0" + dependencies: + csstype: "npm:^3.2.2" + checksum: 10c0/fbcabf303f935ca9f36ed2dcf81751cf84744098c5a683c8a62a0fa7edd31714b48d5c46b12fd0014f7f2dfe4d9ec6de428a850748eec719a70294e51029551b + languageName: node + linkType: hard + "@types/stopword@npm:^2.0.3": version: 2.0.3 resolution: "@types/stopword@npm:2.0.3" @@ -1622,9 +1631,9 @@ __metadata: "@swc/core": "npm:^1.16.2" "@types/d3-array": "npm:^3.2.2" "@types/d3-time-format": "npm:^4.0.3" - "@types/react": "npm:^19.2.18" + "@types/react": "npm:^19.3.0" "@types/react-big-calendar": "npm:^1.16.3" - "@types/react-dom": "npm:^19.2.7" + "@types/react-dom": "npm:^19.3.0" "@types/react-redux": "npm:^7.1.34" "@types/stopword": "npm:^2.0.3" "@visx/axis": "npm:^4.0.0" @@ -1659,15 +1668,15 @@ __metadata: promise-polyfill: "npm:^8.3.0" prop-types: "npm:^15.8.1" quill: "npm:^2.0.3" - react: "npm:^19.2.8" + react: "npm:^19.3.0" react-big-calendar: "npm:^1.20.0" react-cookie-consent: "npm:^10.0.2" - react-dom: "npm:^19.2.8" + react-dom: "npm:^19.3.0" react-grid-system: "npm:^8.2.1" react-i18next: "npm:^17.0.13" react-on-rails: "npm:17.0.1" react-redux: "npm:^9.3.0" - react-refresh: "npm:^0.18.0" + react-refresh: "npm:^0.19.0" react-router: "npm:^8.3.1" react-spring: "npm:10.0.4" react-virtualized: "npm:^9.22.6" @@ -3066,14 +3075,14 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^19.2.8": - version: 19.2.8 - resolution: "react-dom@npm:19.2.8" +"react-dom@npm:^19.3.0": + version: 19.3.0 + resolution: "react-dom@npm:19.3.0" dependencies: - scheduler: "npm:^0.27.0" + scheduler: "npm:^0.28.0" peerDependencies: - react: ^19.2.8 - checksum: 10c0/41ba2247b76f687fcfe5bbc99f514d6b851d8c8041c2f5ded36ed05bd7fdc5208cacbac9de51e3e6633e77f96f44cec9f0d4a5a55184dba4e00738f224439134 + react: ^19.3.0 + checksum: 10c0/c513044b724dd6bfe9500a9682d60973e7f8a48cffba00b6b16d3e910e27dcc459bf71bf0a5c33d7d62a3843ca30de1bd1e2098058aa4ed71f618a24b6061da5 languageName: node linkType: hard @@ -3179,10 +3188,10 @@ __metadata: languageName: node linkType: hard -"react-refresh@npm:^0.18.0": - version: 0.18.0 - resolution: "react-refresh@npm:0.18.0" - checksum: 10c0/34a262f7fd803433a534f50deb27a148112a81adcae440c7d1cbae7ef14d21ea8f2b3d783e858cb7698968183b77755a38b4d4b5b1d79b4f4689c2f6d358fff2 +"react-refresh@npm:^0.19.0": + version: 0.19.0 + resolution: "react-refresh@npm:0.19.0" + checksum: 10c0/8c7af6d165fefdd9f01f62a62e5a3f32c853faec083f67d059f597cd5dbd513004201f5778eb9928dea304dc96fc47f49e9e58684833d305854ecb6b42e91df5 languageName: node linkType: hard @@ -3272,10 +3281,10 @@ __metadata: languageName: node linkType: hard -"react@npm:^19.2.8": - version: 19.2.8 - resolution: "react@npm:19.2.8" - checksum: 10c0/5f86bdb56426652fd6d989d30a6f2e603c057272c47c9ca3a3fbe190a3a39ee9ccce937d63cfc039717abed1b8891d6a499134bc35311acc07eafdacd86537cd +"react@npm:^19.3.0": + version: 19.3.0 + resolution: "react@npm:19.3.0" + checksum: 10c0/dd3099c3c43a7122bc01223bb26b0f8d5ba13cca569af6fb94a005fb54fa73223c9cb203bb226d40cfd49a82cbeef2e36ce84062a3a6d5b9fe8f4437694c7b52 languageName: node linkType: hard @@ -3399,10 +3408,10 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.27.0": - version: 0.27.0 - resolution: "scheduler@npm:0.27.0" - checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 +"scheduler@npm:^0.28.0": + version: 0.28.0 + resolution: "scheduler@npm:0.28.0" + checksum: 10c0/783c8e5ce89d41273a02ed6dc0e3d24bd60e05c9e8f7e3adfc6c82e7c69935500d90e343f1ee26d592fc7504e8a2532573c6368def3e93bfe812391fa53efaf4 languageName: node linkType: hard From c90591b29c0ef56d342a1998901b1290d6252e5e Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Sun, 13 Sep 2026 01:19:55 +0000 Subject: [PATCH 11/28] useDirtyStatus hook is in progress --- .../BingoBoards/BingoGameDataAdmin.tsx | 16 ++- .../BingoBoards/CandidateListEntry.tsx | 14 +-- app/javascript/components/ConceptsTable.tsx | 24 ++-- .../assignments/AssignmentDataAdmin.tsx | 11 +- .../components/checkin/InstallmentReport.tsx | 12 +- .../infrastructure/DirtyIndicator.tsx | 74 ++++++----- .../components/infrastructure/StatusSlice.ts | 118 ++++++++++++------ .../components/profile/ProfileDataAdmin.tsx | 108 ++++++++-------- tsconfig.json | 2 +- 9 files changed, 218 insertions(+), 161 deletions(-) diff --git a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx index 62daabdb2..89bc50557 100644 --- a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx +++ b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx @@ -10,7 +10,7 @@ import { Button } from "primereact/button"; import { useTranslation } from "react-i18next"; import { useTypedSelector } from "../infrastructure/AppReducers"; -import { startTask, endTask, useDirtyStatus } from "../infrastructure/StatusSlice"; +import { startTask, endTask, useDirtyStatus, DIRTY_STATUS } from "../infrastructure/StatusSlice"; import axios from "axios"; import { Editor } from "primereact/editor"; import EditorToolbar from "../toolbars/EditorToolbar"; @@ -41,9 +41,8 @@ export default function BingoGameDataAdmin(props) { const { t, i18n } = useTranslation(`${category}s`); - const [dirty, setDirty] = useState(false); + const [dirty, setDirty] = useDirtyStatus(); const suppressDirtyRef = useRef(false); - useDirtyStatus(category, dirty); const [curTab, setCurTab] = useState(0); const [messages, setMessages] = useState({}); const [gameProjects, setGameProjects] = useState([ @@ -88,10 +87,10 @@ export default function BingoGameDataAdmin(props) { useEffect(() => { if (suppressDirtyRef.current) { - suppressDirtyRef.current = false; + //suppressDirtyRef.current = false; return; } - setDirty(true); + setDirty(DIRTY_STATUS.DIRTY); }, [ gameTopic, gameDescriptionEditor, @@ -167,10 +166,8 @@ export default function BingoGameDataAdmin(props) { setGameGroupDiscount(bingo_game.group_discount || 0); setGameGroupProjectId(bingo_game.project_id); setFoundWords(data.found_words); - setDirty(false); + setDirty(DIRTY_STATUS.CLEAN); - //getBingoGameData(); - //setDirty(false); navigate(`../${courseIdParam}/bingo_game/${bingoGameId}`, { replace: true }); @@ -247,13 +244,14 @@ export default function BingoGameDataAdmin(props) { setGameGroupDiscount(bingo_game.group_discount || 0); setGameGroupProjectId(bingo_game.project_id); setFoundWords(data.found_words); - setDirty(false); + setDirty(DIRTY_STATUS.CLEAN); }) .catch(error => { console.log("error", error); return [{ id: -1, name: "no data" }]; }) .finally(() => { + suppressDirtyRef.current = false; dispatch(endTask()); }); }; diff --git a/app/javascript/components/BingoBoards/CandidateListEntry.tsx b/app/javascript/components/BingoBoards/CandidateListEntry.tsx index c077fd8c0..09fb9794b 100644 --- a/app/javascript/components/BingoBoards/CandidateListEntry.tsx +++ b/app/javascript/components/BingoBoards/CandidateListEntry.tsx @@ -11,7 +11,8 @@ import { endTask, addMessage, Priorities, - useDirtyStatus + useDirtyStatus, + DIRTY_STATUS } from "../infrastructure/StatusSlice"; import { useTypedSelector } from "../infrastructure/AppReducers"; import axios from "axios"; @@ -39,10 +40,9 @@ export default function CandidateListEntry(props: Props) { const { bingoGameId } = useParams(); - const [dirty, setDirty] = useState(false); + const [dirty, setDirty] = useDirtyStatus(); const suppressDirtyRef = useRef(false); const dispatch = useDispatch(); - useDirtyStatus(category, dirty); const [candidateListId, setCandidateListId] = useState(0); const [topic, setTopic] = useState(""); @@ -84,7 +84,7 @@ export default function CandidateListEntry(props: Props) { setHelpRequested(data.help_requested); setRequestCollaborationUrl(data.request_collaboration_url); - setDirty(false); + setDirty(DIRTY_STATUS.CLEAN); }) .catch(error => { console.log("error", error); @@ -162,7 +162,7 @@ export default function CandidateListEntry(props: Props) { setHelpRequested(data.help_requested); setOthersRequestedHelp(data.others_requested_help); - setDirty(false); + setDirty(DIRTY_STATUS.CLEAN); dispatch(addMessage(data.messages.main, new Date(), Priorities.INFO)); } else { data.messages.forEach(message => { @@ -189,7 +189,7 @@ export default function CandidateListEntry(props: Props) { suppressDirtyRef.current = false; return; } - setDirty(true); + setDirty(DIRTY_STATUS.DIRTY); }, [candidates]); // TODO: Fix the check to see if the form is dirty @@ -216,7 +216,7 @@ export default function CandidateListEntry(props: Props) { setCandidates(prepCandidates(data.candidates, data.expected_count)); setHelpRequested(data.help_requested); setOthersRequestedHelp(data.others_requested_help); - setDirty(false); + setDirty(DIRTY_STATUS.CLEAN); }) .catch(error => { console.log("error", error); diff --git a/app/javascript/components/ConceptsTable.tsx b/app/javascript/components/ConceptsTable.tsx index 2c31e4357..322bc5a05 100644 --- a/app/javascript/components/ConceptsTable.tsx +++ b/app/javascript/components/ConceptsTable.tsx @@ -15,7 +15,7 @@ import { Column } from "primereact/column"; import { Dialog } from "primereact/dialog"; import { useDispatch } from "react-redux"; -import { startTask, endTask, addMessage, Priorities, useDirtyStatus } from "./infrastructure/StatusSlice"; +import { startTask, endTask, addMessage, Priorities, useDirtyStatus, DIRTY_STATUS } from "./infrastructure/StatusSlice"; import { InputText } from "primereact/inputtext"; enum OPT_COLS { @@ -50,9 +50,15 @@ export default function ConceptsTable() { const [sortBy, setSortBy] = useState("name"); const [sortDirection, setSortDirection] = useState(SortDirection.DESC); + const [dirty, setDirty] = useDirtyStatus(); const [editing, setEditing] = useState(false); - const [dirty, setDirty] = useState(false); - useDirtyStatus(category, dirty); + const startEditing = () => { + setEditing(true); + } + const stopEditing = () => { + setEditing(false); + setDirty( DIRTY_STATUS.CLEAN ); + } const [conceptName, setConceptName] = useState(""); const [conceptId, setConceptId] = useState(-1); @@ -64,7 +70,7 @@ export default function ConceptsTable() { const setName = newName => { setConceptName(newName); - setDirty(true); + setDirty(DIRTY_STATUS.DIRTY); }; const getConcepts = () => { @@ -89,8 +95,8 @@ export default function ConceptsTable() { const drillDown = event => { setConceptId(event.data.id); setConceptName(event.data.name); - setEditing(true); - setDirty(false); + startEditing(); + setDirty(DIRTY_STATUS.CLEAN); }; const updateConcept = (id, name) => { dispatch(startTask("load")); @@ -112,7 +118,7 @@ export default function ConceptsTable() { setConcepts(tmpConcepts); setConceptsRaw(tmpConcepts); //statusActions.endTask("load"); - setEditing(false); + stopEditing(); dispatch( addMessage(t("update_success"), new Date(), Priorities.INFO ) ); }) .catch(error => { @@ -196,12 +202,12 @@ export default function ConceptsTable() { setEditing(false)} + onHide={() => stopEditing()} aria-labelledby="edit" header={t("edit.title")} footer={ <> - + ) : null; diff --git a/app/javascript/components/projects/ProjectGroups.tsx b/app/javascript/components/projects/ProjectGroups.tsx index 084ba9285..cdb86f609 100644 --- a/app/javascript/components/projects/ProjectGroups.tsx +++ b/app/javascript/components/projects/ProjectGroups.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useMemo } from "react"; import axios from "axios"; -import { startTask, endTask } from "../infrastructure/StatusSlice"; +import { startTask, endTask, useDirtyStatus, DIRTY_STATUS } from "../infrastructure/StatusSlice"; import { IUser } from '../infrastructure/ProfileSlice'; import { useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; @@ -36,7 +36,7 @@ export default function ProjectGroups(props: Props) { const category = "projects"; const { t } = useTranslation(category); - const [dirty, setDirty] = useState(false); + const [dirty, setDirty] = useDirtyStatus(); const [working, setWorking] = useState(true); const [message, setMessage] = useState(""); const [filterText, setFilterText] = useState(""); @@ -87,7 +87,7 @@ export default function ProjectGroups(props: Props) { }, []); const setGroup = (student_id: number, group_id: number) => { - setDirty(true); + setDirty(DIRTY_STATUS.DIRTY); setStudentsRaw(prev => ({ ...prev, [student_id]: { @@ -99,7 +99,7 @@ export default function ProjectGroups(props: Props) { const setGroupName = (event: React.ChangeEvent, group_id: number) => { const newName = event.target.value; - setDirty(true); + setDirty(DIRTY_STATUS.DIRTY); setGroupsRaw(prev => ({ ...prev, [group_id]: { @@ -124,11 +124,11 @@ export default function ProjectGroups(props: Props) { } }; }); - setDirty(true); + setDirty(DIRTY_STATUS.DIRTY); }; const removeGroup = (event, group_id: number) => { - setDirty(true); + setDirty(DIRTY_STATUS.DIRTY); setStudentsRaw(prev => { const updated = { ...prev }; Object.values(updated).forEach(student => { @@ -224,7 +224,6 @@ export default function ProjectGroups(props: Props) { .then(response => { const data = response.data; setWorking(false); - setDirty(false); setSuggestedGroupsRaw(null); setSuggestedStudentsRaw(null); setSuggestedGroups([]); @@ -232,6 +231,7 @@ export default function ProjectGroups(props: Props) { setGroupsRaw(data.groups); setStudentsRaw(data.students); setMessage(data.message == null ? "" : data.message); + setDirty(DIRTY_STATUS.CLEAN); }) .catch(error => { console.log("error", error); From 917de6d1217355c3a308f411dd5eac2c30ccf159 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Sun, 13 Sep 2026 20:15:43 -0400 Subject: [PATCH 14/28] versions --- Gemfile.lock | 4 ++-- mise.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b44b14ebf..b61ecd84a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -396,7 +396,7 @@ GEM ostruct (0.6.3) package_json (0.2.0) parallel (2.2.0) - parallel_tests (5.7.0) + parallel_tests (5.8.0) parallel parser (3.3.12.0) ast (~> 2.4.1) @@ -880,7 +880,7 @@ CHECKSUMS ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 package_json (0.2.0) sha256=92c022dc2999a9e57e835f1ec1a84c9a6c2be08e0fd68886c6f86d94101b6b4d parallel (2.2.0) sha256=e1059c5fd7b649558a0aec38a769f06a42942bdb40503d005a59c352fe011cd8 - parallel_tests (5.7.0) sha256=3f1762c46ca2c223b8af8ef877217f9d76974e191bfa934f2580b58bcf1d005c + parallel_tests (5.8.0) sha256=181898cbee2ffb1c796274ce31d27f792a421324609bfc7e9b5531d929d91eda parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828 pdf-core (0.10.0) sha256=0a5d101e2063c01e3f941e1ee47cbb97f1adfc1395b58372f4f65f1300f3ce91 powerpoint (1.8) diff --git a/mise.toml b/mise.toml index fd98837f2..d7c6deeb1 100644 --- a/mise.toml +++ b/mise.toml @@ -5,5 +5,5 @@ podman = { version = "6.1.1", rename_exe = "podman" } python = "3.13.7" ruby = "4.0.6" rust = "1.97.1" -usage = "6.8.0" +usage = "6.9.0" yarn = "4.18.0" From 8e4a51fb0a285442d3cd5ce0bb7d86d3f3b9e11a Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Mon, 14 Sep 2026 02:45:18 +0000 Subject: [PATCH 15/28] some fixes --- .../components/assignments/RubricDataAdmin.tsx | 4 ---- .../components/course_admin/CourseDataAdmin.tsx | 4 ++-- .../components/infrastructure/StatusSlice.ts | 5 ----- .../components/profile/ProfileDataAdmin.tsx | 12 ++++++------ .../components/projects/ProjectDataAdmin.tsx | 11 +++++------ 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/app/javascript/components/assignments/RubricDataAdmin.tsx b/app/javascript/components/assignments/RubricDataAdmin.tsx index f83d7b313..4eb00a72d 100644 --- a/app/javascript/components/assignments/RubricDataAdmin.tsx +++ b/app/javascript/components/assignments/RubricDataAdmin.tsx @@ -40,10 +40,6 @@ export default function RubricDataAdmin(props) { return null != state.profile.lastRetrieved; }); - const dirty = useTypedSelector(state => { - return state.status.dirtyStatus[category]; - }); - const dispatch = useDispatch(); const freshCriteria = { description: "New Criteria", diff --git a/app/javascript/components/course_admin/CourseDataAdmin.tsx b/app/javascript/components/course_admin/CourseDataAdmin.tsx index 216aabff4..81b6bd2c6 100644 --- a/app/javascript/components/course_admin/CourseDataAdmin.tsx +++ b/app/javascript/components/course_admin/CourseDataAdmin.tsx @@ -68,8 +68,6 @@ export default function CourseDataAdmin() { ); const [curTab, setCurTab] = useState(0); - const [dirty, setDirty] = useDirtyStatus( courseId === null || courseId === undefined ? DIRTY_STATUS.DIRTY : DIRTY_STATUS.CLEAN ); - const suppressDirtyRef = React.useRef(false); const [messages, setMessages] = useState({}); let { courseIdParam } = useParams(); @@ -77,6 +75,8 @@ export default function CourseDataAdmin() { const [courseId, setCourseId] = useState( parseInt("new" === courseIdParam ? null : courseIdParam) ); + const [dirty, setDirty] = useDirtyStatus( courseId === null || courseId === undefined ? DIRTY_STATUS.DIRTY : DIRTY_STATUS.CLEAN ); + const suppressDirtyRef = React.useRef(false); const [course, setCourse] = useState({ id: courseId, diff --git a/app/javascript/components/infrastructure/StatusSlice.ts b/app/javascript/components/infrastructure/StatusSlice.ts index 1e18a9963..89f6961b1 100644 --- a/app/javascript/components/infrastructure/StatusSlice.ts +++ b/app/javascript/components/infrastructure/StatusSlice.ts @@ -118,11 +118,6 @@ export function useDirtyStatus( const isDirty = useSelector((state: RootState) => { const status = state.status.dirtyStatus as Record; - console.log( - `status for ${dataPanel || "default"}`, - status[dataPanel || ""], - ); - switch (status[dataPanel || ""]) { case null: case undefined: diff --git a/app/javascript/components/profile/ProfileDataAdmin.tsx b/app/javascript/components/profile/ProfileDataAdmin.tsx index 83bac4e6c..599e21e4d 100644 --- a/app/javascript/components/profile/ProfileDataAdmin.tsx +++ b/app/javascript/components/profile/ProfileDataAdmin.tsx @@ -132,10 +132,7 @@ export default function ProfileDataAdmin(props: Props) { const navigate = useNavigate(); - console.log( dirty ); - const blocker = useBlocker((args) => { - console.log(`ProfileDataAdmin: useBlocker: dirty=${dirty}, nextLocation=${args.nextLocation.pathname}, state=${args.nextLocation.state}`); if (args.nextLocation.state === 'unblocked' || !dirty) { return false; } else { @@ -431,11 +428,14 @@ export default function ProfileDataAdmin(props: Props) { ] = useState(languages); const saveButton = useMemo( - () => ( - - ), + ) + }, [dirty, user.welcomed, tourCompleted] ); diff --git a/app/javascript/components/projects/ProjectDataAdmin.tsx b/app/javascript/components/projects/ProjectDataAdmin.tsx index aea6ffd86..cbd32c1f8 100644 --- a/app/javascript/components/projects/ProjectDataAdmin.tsx +++ b/app/javascript/components/projects/ProjectDataAdmin.tsx @@ -9,8 +9,6 @@ import { useDispatch } from "react-redux"; import { startTask, endTask, - setDirty, - setClean, addMessage, Priorities, useDirtyStatus, @@ -51,8 +49,6 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) { const { courseIdParam, projectIdParam } = useParams(); const [curTab, setCurTab] = useState(0); - const [dirty, setDirty] = useDirtyStatus( projectIdParam === "new" ? DIRTY_STATUS.DIRTY : DIRTY_STATUS.CLEAN ); - const suppressDirtyRef = useRef(false); const [messages, setMessages] = useState({}); const dispatch = useDispatch(); @@ -63,6 +59,9 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) { const [projectId, setProjectId] = useState( "new" === projectIdParam ? null : Number(projectIdParam) ); + const [dirty, setDirty] = useDirtyStatus( projectIdParam === "new" ? DIRTY_STATUS.DIRTY : DIRTY_STATUS.CLEAN ); + const suppressDirtyRef = useRef(false); + const [projectName, setProjectName] = useState(""); const [projectDescription, setProjectDescription] = useState(""); const now = new Date(); @@ -93,6 +92,7 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) { } else { url = url + projectId + ".json"; } + suppressDirtyRef.current = true; axios .get(url, {}) .then(response => { @@ -122,6 +122,7 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) { }) .finally(() => { dispatch(endTask()); + suppressDirtyRef.current = false; }); }; const saveProject = () => { @@ -174,7 +175,6 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) { const course = data.course; setCourseName(course.name); - dispatch(setClean(category)); setMessages(data.messages); dispatch( addMessage(data.messages.status, new Date(), Priorities.INFO) @@ -211,7 +211,6 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) { useEffect(() => { if (suppressDirtyRef.current || projectId == null) { - suppressDirtyRef.current = false; return; } setDirty(DIRTY_STATUS.DIRTY); From 8bda9314f24fd488260f98e3e998bcd33e345201 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Mon, 14 Sep 2026 16:07:47 +0000 Subject: [PATCH 16/28] fix for profile editing --- app/javascript/components/profile/ProfileDataAdmin.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/javascript/components/profile/ProfileDataAdmin.tsx b/app/javascript/components/profile/ProfileDataAdmin.tsx index 599e21e4d..49dc07687 100644 --- a/app/javascript/components/profile/ProfileDataAdmin.tsx +++ b/app/javascript/components/profile/ProfileDataAdmin.tsx @@ -429,9 +429,8 @@ export default function ProfileDataAdmin(props: Props) { const saveButton = useMemo( () => { - console.log(`ProfileDataAdmin: saveButton: dirty=${dirty}, user.welcomed=${user.welcomed}, tourCompleted=${tourCompleted}`); return ( - ) From 2dfac943068b4ee23a3d2627f0f469cbff2b11b7 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Mon, 14 Sep 2026 18:23:40 +0000 Subject: [PATCH 17/28] submit button shows properly now --- app/javascript/components/checkin/InstallmentReport.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/components/checkin/InstallmentReport.tsx b/app/javascript/components/checkin/InstallmentReport.tsx index 403ece364..5ea235a73 100644 --- a/app/javascript/components/checkin/InstallmentReport.tsx +++ b/app/javascript/components/checkin/InstallmentReport.tsx @@ -102,6 +102,8 @@ export default function InstallmentReport(props: Props) { }; useEffect(() => { + console.log("InstallmentReport: dirty", dirty); + console.log("SuppressDirtyRef.current", suppressDirtyRef.current); if (suppressDirtyRef.current) { suppressDirtyRef.current = false; return; @@ -126,8 +128,10 @@ export default function InstallmentReport(props: Props) { return retVal; }; + console.log( 'dirty', dirty ); + console.log( 'not new', installment.id, Boolean(installment.id)) const saveButton = ( - ); @@ -202,7 +206,6 @@ export default function InstallmentReport(props: Props) { setDirty(DIRTY_STATUS.CLEAN); setGroup(data.group); - suppressDirtyRef.current = true; setProject(data.installment.project); }) .catch(error => { @@ -210,6 +213,7 @@ export default function InstallmentReport(props: Props) { }) .finally(() => { dispatch(endTask()); + suppressDirtyRef.current = false; }); }; //Store what we've got From 9812dde9481a661b31a26add3bfcdc147d50f19c Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Tue, 15 Sep 2026 01:43:23 +0000 Subject: [PATCH 18/28] versions --- Gemfile.lock | 18 +++++++++--------- package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b61ecd84a..04f0156f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,8 +108,8 @@ GEM auth-sanitizer (0.2.3) version_gem (~> 1.1, >= 1.1.14) aws-eventstream (1.4.0) - aws-partitions (1.1286.0) - aws-sdk-core (3.256.0) + aws-partitions (1.1287.0) + aws-sdk-core (3.257.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -289,7 +289,7 @@ GEM i18n_data (1.1.0) simple_po_parser (~> 1.1) image_processing (2.1.0) - io-console (0.9.2) + io-console (0.9.3) irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) @@ -563,9 +563,9 @@ GEM rack (>= 1.1) rubocop (>= 1.89.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-thread_safety (0.7.3) + rubocop-thread_safety (0.8.0) lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) + rubocop (~> 1.89) rubocop-ast (>= 1.44.0, < 2.0) ruby-lsp (0.26.11) language_server-protocol (~> 3.17.0) @@ -753,8 +753,8 @@ CHECKSUMS ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 auth-sanitizer (0.2.3) sha256=db10aac92cfbe4c64ab637eebcbe1d67395d1694798041362173370f59933e3c aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b - aws-partitions (1.1286.0) sha256=955672fb6cbeccff69b4566a32147d9b8b899afe59220ca786dc60a798f18950 - aws-sdk-core (3.256.0) sha256=54680a6818323ad1a977dd2aab88a98f294f11fd603852af53e5718e9ce11a12 + aws-partitions (1.1287.0) sha256=76f4c73011654553566c5db5bbef0f24738bb016e923910e8b1b3f9cb7d989ca + aws-sdk-core (3.257.0) sha256=a92ad19c7ff668cec6688ee3200ee562da80647a3bbeed9fda02e8b0ca6492c6 aws-sdk-kms (1.132.0) sha256=094b0097bb3be9d5c1ac87e971ca7f5aebb746801dfb5f0f576480bf98dca69c aws-sdk-s3 (1.232.0) sha256=3899ee76a776761001a22b0e6992d2f4683e03c08f2399835b8ea32c0bb44ee7 aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 @@ -833,7 +833,7 @@ CHECKSUMS i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 i18n_data (1.1.0) sha256=fb7ae43bd03ec4dc2e6e62e726aac05aa868c170ba1444c056e2ab0d872bb2ed image_processing (2.1.0) sha256=ca3814b1b909fc5fab68b261c50eaffef11e29a6907143e9cf1efc14b6dc6f47 - io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08 + io-console (0.9.3) sha256=f555049461b0afb78a5448b5be8e7c7b48371347cd48abfc22814764160000dd irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 jbuilder (2.15.1) sha256=2430bec28fb0cebacb5875b1009cf9d8bc3c303ccb810c4c8b062a4b51457637 jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 @@ -935,7 +935,7 @@ CHECKSUMS rubocop-capybara (3.0.0) sha256=7a64655238acda7f8f3c87e37ac825a64c615a79c17c253f1a28270dc3768c4b rubocop-performance (1.27.0) sha256=eeeb1374d062a368ee1c787b70eb0b0cc4b184cb1f8565f424760946146d61ce rubocop-rails (2.37.0) sha256=6e1645add5060e0328f8ddda0d820f55697c591394398bf14bb9dccb62f14b7e - rubocop-thread_safety (0.7.3) sha256=067cdd52fbf5deffc18995437e45b5194236eaff4f71de3375a1f6052e48f431 + rubocop-thread_safety (0.8.0) sha256=71fe5e7894fb9acfbd4c2dcd394feabc35bd690f75b0bc846e1504d9e31f9034 ruby-lsp (0.26.11) sha256=4cc8f1587237ff8c9031680b5491df901a82559eeaa1ae96863ffe700b32583d ruby-lsp-rails (0.4.8) sha256=f09d1f926d4063deeb2f3049311925c20dfe6c912371e3bcd04a265a865c44ae ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 diff --git a/package.json b/package.json index 846f2d579..eb0c384d9 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "react-cookie-consent": "^10.0.2", "react-dom": "^19.3.0", "react-grid-system": "^8.2.1", - "react-i18next": "^17.0.13", + "react-i18next": "^17.0.14", "react-on-rails": "17.0.1", "react-redux": "^9.3.0", "react-router": "^8.3.1", diff --git a/yarn.lock b/yarn.lock index 323880681..dae96fe77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1673,7 +1673,7 @@ __metadata: react-cookie-consent: "npm:^10.0.2" react-dom: "npm:^19.3.0" react-grid-system: "npm:^8.2.1" - react-i18next: "npm:^17.0.13" + react-i18next: "npm:^17.0.14" react-on-rails: "npm:17.0.1" react-redux: "npm:^9.3.0" react-refresh: "npm:^0.19.0" @@ -3097,9 +3097,9 @@ __metadata: languageName: node linkType: hard -"react-i18next@npm:^17.0.13": - version: 17.0.13 - resolution: "react-i18next@npm:17.0.13" +"react-i18next@npm:^17.0.14": + version: 17.0.14 + resolution: "react-i18next@npm:17.0.14" dependencies: "@babel/runtime": "npm:^7.29.7" html-parse-stringify: "npm:^4.0.1" @@ -3115,7 +3115,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10c0/58369886b4b49dd1c3c950de8d89811a15f1728f0805356f5003ec77ea96f2ef007ef25fb039fb241676d9f48664448d8be9dbc569bebfa06ad1cad5a76517ec + checksum: 10c0/9427dd6e560b25087b704ecc8abf823b099b8c8db787764e28f37b32950e1ff32a8da6142295ee05c57cf8270a6a8dc77bde06d2b75d69ec0df519498b367552 languageName: node linkType: hard From 0e213116a9f86c10dc376bb900a7280546ba40c2 Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Tue, 15 Sep 2026 12:03:49 +0000 Subject: [PATCH 19/28] Button errors resolved. --- .../components/BingoBoards/BingoGameDataAdmin.tsx | 7 +++---- app/javascript/components/projects/ProjectDataAdmin.tsx | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx index 73a6180d4..1ab908f1d 100644 --- a/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx +++ b/app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx @@ -87,7 +87,6 @@ export default function BingoGameDataAdmin(props) { useEffect(() => { if (suppressDirtyRef.current || null === bingoGameId ) { - //suppressDirtyRef.current = false; return; } setDirty(DIRTY_STATUS.DIRTY); @@ -264,11 +263,11 @@ export default function BingoGameDataAdmin(props) { return deadline; }, [gameEndDate, gameLeadTime]); - const save_btn = dirty ? ( + const save_btn = ( }> - ) : null; + ); const group_options = gameGroupOption ? ( }> diff --git a/app/javascript/components/projects/ProjectDataAdmin.tsx b/app/javascript/components/projects/ProjectDataAdmin.tsx index cbd32c1f8..f2ba37b68 100644 --- a/app/javascript/components/projects/ProjectDataAdmin.tsx +++ b/app/javascript/components/projects/ProjectDataAdmin.tsx @@ -226,11 +226,11 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) { projectEndDOW ]); - const saveButton = dirty ? ( - - ) : null; + ); //Later I want to call the activate/deactivate right here const toggleActive = () => { From 8f239f63e10db5d4f856e61e2640192add37498c Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Wed, 16 Sep 2026 02:30:17 +0000 Subject: [PATCH 20/28] dirty works! --- .../course_admin/CourseDataAdmin.tsx | 31 ++++++++++--------- config/locales/courses.en.yml | 5 +++ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/app/javascript/components/course_admin/CourseDataAdmin.tsx b/app/javascript/components/course_admin/CourseDataAdmin.tsx index 81b6bd2c6..0f60bd22d 100644 --- a/app/javascript/components/course_admin/CourseDataAdmin.tsx +++ b/app/javascript/components/course_admin/CourseDataAdmin.tsx @@ -139,7 +139,6 @@ export default function CourseDataAdmin() { } setCourse(localCourse); - suppressDirtyRef.current = false; setDirty(DIRTY_STATUS.CLEAN); }) @@ -147,6 +146,7 @@ export default function CourseDataAdmin() { console.log("error:", error); }).finally(() => { dispatch(endTask()); + suppressDirtyRef.current = false; }) }; @@ -158,6 +158,7 @@ export default function CourseDataAdmin() { ? `${endpoints.baseUrl}/new.json` : `${endpoints.baseUrl}/${courseId}.json`; + suppressDirtyRef.current = true; axios({ method: method, url: url, @@ -204,9 +205,6 @@ export default function CourseDataAdmin() { setCourse(localCourse); setCourseId(localCourse.id); navigate(`../${localCourse.id}`, { replace: true }); - - - dispatch(setClean(category)); } postNewMessage(data.messages); }) @@ -214,6 +212,7 @@ export default function CourseDataAdmin() { console.log("error:", error); }).finally(() => { dispatch(endTask("saving")); + suppressDirtyRef.current = false; }) }; @@ -235,12 +234,14 @@ export default function CourseDataAdmin() { }, [endpointStatus]); useEffect(() => { - if (!suppressDirtyRef.current || courseId === null || courseId === undefined) { + if (suppressDirtyRef.current ) { return; } setDirty(DIRTY_STATUS.DIRTY); }, [ course, + course.id, course.number, course.description, course.start_date, course.end_date, + course.school_id, course.consent_form_id, course.timezone ]); const postNewMessage = msgs => { @@ -248,14 +249,14 @@ export default function CourseDataAdmin() { setMessages(msgs); }; - const saveButton = dirty ? ( + const saveButton = (
-
- ) : null; + ); const setCourseValue = (field, value) => { setCourse(course => { @@ -269,7 +270,7 @@ export default function CourseDataAdmin() { {t('edit.number')} { @@ -284,7 +285,7 @@ export default function CourseDataAdmin() { {t('edit.name')} { @@ -299,7 +300,7 @@ export default function CourseDataAdmin() { {t('edit.description')} { @@ -337,7 +338,7 @@ export default function CourseDataAdmin() { }} optionLabel="name" optionValue="id" - placeholder="Select a School" + placeholder={t('edit.select_school_plchldr')} showClear={false} /> ) : ( @@ -361,7 +362,7 @@ export default function CourseDataAdmin() { }} optionLabel="name" optionValue="name" - placeholder="Select a Time Zone" + placeholder={t('edit.selct_timezone_plchldr')} showClear={false} /> ) : ( @@ -385,7 +386,7 @@ export default function CourseDataAdmin() { }} optionValue="id" optionLabel="name" - placeholder="Select a Consent Form" + placeholder={t('edit.select_consent_form_plchldr')} showClear={true} /> diff --git a/config/locales/courses.en.yml b/config/locales/courses.en.yml index 429dcecd3..10f812b4e 100644 --- a/config/locales/courses.en.yml +++ b/config/locales/courses.en.yml @@ -4,6 +4,8 @@ en: btn_tooltip_title: Create a copy based on this course copy_btn_aria: Make a copy copy_btn_txt: Make a Copy + create_btn: Create Course + save_btn: Save Course size_btn: large date_picker_label: New course start date? toggle_columns_plc: Show hidden columns @@ -52,6 +54,9 @@ en: school: School time_zone: Time Zone consent: Consent Form + select_consent_form_plchldr: Select a Consent Form + selct_school_plchldr: Select a School + selct_timezone_plchldr: Select a Time Zone dates_tz: All dates shown in {{time_zone}} timezone. dates: Course Dates download_qr: Download self-registration code From cdb27407ad5c34729ddb32bd703c019d6d628fbc Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Wed, 16 Sep 2026 02:35:07 +0000 Subject: [PATCH 21/28] version bumps --- Gemfile.lock | 4 +- mise.toml | 2 +- package.json | 4 +- yarn.lock | 144 +++++++++++++++++++++++++-------------------------- 4 files changed, 77 insertions(+), 77 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 04f0156f4..358835566 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -262,7 +262,7 @@ GEM json (>= 2) faker (3.8.0) i18n (>= 1.8.11, < 2) - faraday (2.14.3) + faraday (2.14.4) faraday-net_http (>= 2.0, < 3.5) json logger @@ -814,7 +814,7 @@ CHECKSUMS erubis (2.7.0) sha256=63653f5174a7997f6f1d6f465fbe1494dcc4bdab1fb8e635f6216989fb1148ba execjs (2.10.2) sha256=466cf2b49e50c0cfdff8fdb88a7a983dfab7ac0f6185c317ccb434a75c224c88 faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc - faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + faraday (2.14.4) sha256=9bb4408c44621b0dbaaaa39e5dbfc4dbe58cc6751b6eb94d16ae9e1f2f2fab6e faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df diff --git a/mise.toml b/mise.toml index d7c6deeb1..0eec71d21 100644 --- a/mise.toml +++ b/mise.toml @@ -5,5 +5,5 @@ podman = { version = "6.1.1", rename_exe = "podman" } python = "3.13.7" ruby = "4.0.6" rust = "1.97.1" -usage = "6.9.0" +usage = "6.9.1" yarn = "4.18.0" diff --git a/package.json b/package.json index eb0c384d9..cfe32dbb2 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "@react-spring/types": "10.1.2", "@react-spring/web": "10.1.2", "@reduxjs/toolkit": "^2.12.0", - "@rspack/cli": "^2.2.3", - "@rspack/core": "^2.2.3", + "@rspack/cli": "^2.2.4", + "@rspack/core": "^2.2.4", "@rspack/dev-server": "^2.2.1", "@rspack/plugin-react-refresh": "^2.0.2", "@swc/core": "^1.16.2", diff --git a/yarn.lock b/yarn.lock index dae96fe77..e06ab9eae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -414,79 +414,79 @@ __metadata: languageName: node linkType: hard -"@rspack/binding-darwin-arm64@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-darwin-arm64@npm:2.2.3" +"@rspack/binding-darwin-arm64@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-darwin-arm64@npm:2.2.4" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-darwin-x64@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-darwin-x64@npm:2.2.3" +"@rspack/binding-darwin-x64@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-darwin-x64@npm:2.2.4" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rspack/binding-linux-arm64-gnu@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-arm64-gnu@npm:2.2.3" +"@rspack/binding-linux-arm64-gnu@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-arm64-gnu@npm:2.2.4" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-arm64-musl@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-arm64-musl@npm:2.2.3" +"@rspack/binding-linux-arm64-musl@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-arm64-musl@npm:2.2.4" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rspack/binding-linux-ppc64-gnu@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-ppc64-gnu@npm:2.2.3" +"@rspack/binding-linux-ppc64-gnu@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-ppc64-gnu@npm:2.2.4" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-riscv64-gnu@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.2.3" +"@rspack/binding-linux-riscv64-gnu@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.2.4" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-riscv64-musl@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-riscv64-musl@npm:2.2.3" +"@rspack/binding-linux-riscv64-musl@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-riscv64-musl@npm:2.2.4" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rspack/binding-linux-s390x-gnu@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-s390x-gnu@npm:2.2.3" +"@rspack/binding-linux-s390x-gnu@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-s390x-gnu@npm:2.2.4" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-x64-gnu@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-x64-gnu@npm:2.2.3" +"@rspack/binding-linux-x64-gnu@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-x64-gnu@npm:2.2.4" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-x64-musl@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-linux-x64-musl@npm:2.2.3" +"@rspack/binding-linux-x64-musl@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-linux-x64-musl@npm:2.2.4" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rspack/binding-wasm32-wasi@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-wasm32-wasi@npm:2.2.3" +"@rspack/binding-wasm32-wasi@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-wasm32-wasi@npm:2.2.4" dependencies: "@emnapi/core": "npm:1.11.3" "@emnapi/runtime": "npm:1.11.3" @@ -495,45 +495,45 @@ __metadata: languageName: node linkType: hard -"@rspack/binding-win32-arm64-msvc@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-win32-arm64-msvc@npm:2.2.3" +"@rspack/binding-win32-arm64-msvc@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-win32-arm64-msvc@npm:2.2.4" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-win32-ia32-msvc@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-win32-ia32-msvc@npm:2.2.3" +"@rspack/binding-win32-ia32-msvc@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-win32-ia32-msvc@npm:2.2.4" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rspack/binding-win32-x64-msvc@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding-win32-x64-msvc@npm:2.2.3" +"@rspack/binding-win32-x64-msvc@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding-win32-x64-msvc@npm:2.2.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rspack/binding@npm:2.2.3": - version: 2.2.3 - resolution: "@rspack/binding@npm:2.2.3" - dependencies: - "@rspack/binding-darwin-arm64": "npm:2.2.3" - "@rspack/binding-darwin-x64": "npm:2.2.3" - "@rspack/binding-linux-arm64-gnu": "npm:2.2.3" - "@rspack/binding-linux-arm64-musl": "npm:2.2.3" - "@rspack/binding-linux-ppc64-gnu": "npm:2.2.3" - "@rspack/binding-linux-riscv64-gnu": "npm:2.2.3" - "@rspack/binding-linux-riscv64-musl": "npm:2.2.3" - "@rspack/binding-linux-s390x-gnu": "npm:2.2.3" - "@rspack/binding-linux-x64-gnu": "npm:2.2.3" - "@rspack/binding-linux-x64-musl": "npm:2.2.3" - "@rspack/binding-wasm32-wasi": "npm:2.2.3" - "@rspack/binding-win32-arm64-msvc": "npm:2.2.3" - "@rspack/binding-win32-ia32-msvc": "npm:2.2.3" - "@rspack/binding-win32-x64-msvc": "npm:2.2.3" +"@rspack/binding@npm:2.2.4": + version: 2.2.4 + resolution: "@rspack/binding@npm:2.2.4" + dependencies: + "@rspack/binding-darwin-arm64": "npm:2.2.4" + "@rspack/binding-darwin-x64": "npm:2.2.4" + "@rspack/binding-linux-arm64-gnu": "npm:2.2.4" + "@rspack/binding-linux-arm64-musl": "npm:2.2.4" + "@rspack/binding-linux-ppc64-gnu": "npm:2.2.4" + "@rspack/binding-linux-riscv64-gnu": "npm:2.2.4" + "@rspack/binding-linux-riscv64-musl": "npm:2.2.4" + "@rspack/binding-linux-s390x-gnu": "npm:2.2.4" + "@rspack/binding-linux-x64-gnu": "npm:2.2.4" + "@rspack/binding-linux-x64-musl": "npm:2.2.4" + "@rspack/binding-wasm32-wasi": "npm:2.2.4" + "@rspack/binding-win32-arm64-msvc": "npm:2.2.4" + "@rspack/binding-win32-ia32-msvc": "npm:2.2.4" + "@rspack/binding-win32-x64-msvc": "npm:2.2.4" dependenciesMeta: "@rspack/binding-darwin-arm64": optional: true @@ -563,13 +563,13 @@ __metadata: optional: true "@rspack/binding-win32-x64-msvc": optional: true - checksum: 10c0/5a2320a5f97c5b201d3591312bbccc332a5f8dfc1a35b04112c88da650865ca93d2330e00ce6b41aaa0c853a3bbf171bd3bea61ac878a8c979d0ce9acc0d06d6 + checksum: 10c0/e39876f5c5f05047f6a27f7f29104d32e5deb087bf16a7aff775fa9e85954f006d73c8323644b88ba260a3d16672aa5d6c4b488eceec7a94b92132280aff1391 languageName: node linkType: hard -"@rspack/cli@npm:^2.2.3": - version: 2.2.3 - resolution: "@rspack/cli@npm:2.2.3" +"@rspack/cli@npm:^2.2.4": + version: 2.2.4 + resolution: "@rspack/cli@npm:2.2.4" peerDependencies: "@rspack/core": ^2.0.0-0 "@rspack/dev-server": ^2.0.0-0 @@ -578,15 +578,15 @@ __metadata: optional: true bin: rspack: ./bin/rspack.js - checksum: 10c0/f6cacd55c220916a8b969bf049567888bd27f7570f55848defd4324e74a8aef5aa9f8c0e18c1d27b4995812ad8a3340a3931c3849fcabc9989280da08a452dec + checksum: 10c0/33fd4a1327e6123dd957911327eb060d0c2a3c052d795aa7b502e8ae383a6e81533029c9d2a58f27fa3c91304d136fff37f26c0010e686283b59e7b7257da190 languageName: node linkType: hard -"@rspack/core@npm:^2.2.3": - version: 2.2.3 - resolution: "@rspack/core@npm:2.2.3" +"@rspack/core@npm:^2.2.4": + version: 2.2.4 + resolution: "@rspack/core@npm:2.2.4" dependencies: - "@rspack/binding": "npm:2.2.3" + "@rspack/binding": "npm:2.2.4" peerDependencies: "@module-federation/runtime-tools": ^0.24.1 || ^2.0.0 "@swc/helpers": ^0.5.23 @@ -595,7 +595,7 @@ __metadata: optional: true "@swc/helpers": optional: true - checksum: 10c0/4a0f8f2b31d3021322db968c4af4471d6114d9dfe6a4587e8fd9413b60a88cb0c0d1923ac5db05b89f255d735ce98465235ea4f5ddb0d02d49cb90fde5fa9c6d + checksum: 10c0/adbf6ff2eed590d0f405484ffdd6096d5454b946ac90dd989a459dacebb370ae05c0a6590b848f3b98666bf58547c558c6752087493ec2ffbb7e707a1a8c45c6 languageName: node linkType: hard @@ -1624,8 +1624,8 @@ __metadata: "@react-spring/types": "npm:10.1.2" "@react-spring/web": "npm:10.1.2" "@reduxjs/toolkit": "npm:^2.12.0" - "@rspack/cli": "npm:^2.2.3" - "@rspack/core": "npm:^2.2.3" + "@rspack/cli": "npm:^2.2.4" + "@rspack/core": "npm:^2.2.4" "@rspack/dev-server": "npm:^2.2.1" "@rspack/plugin-react-refresh": "npm:^2.0.2" "@swc/core": "npm:^1.16.2" From f55d5cf980cebaf024f9a12b43d75fd39a4345ef Mon Sep 17 00:00:00 2001 From: Micah Gideon Modell Date: Thu, 17 Sep 2026 03:08:23 +0000 Subject: [PATCH 22/28] fix for dirty --- .../assignments/AssignmentDataAdmin.tsx | 18 ++++++++++++------ .../infrastructure/DirtyIndicator.tsx | 5 +++-- .../components/infrastructure/StatusSlice.ts | 5 ++++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/javascript/components/assignments/AssignmentDataAdmin.tsx b/app/javascript/components/assignments/AssignmentDataAdmin.tsx index 627048c3d..ea2c45cfc 100644 --- a/app/javascript/components/assignments/AssignmentDataAdmin.tsx +++ b/app/javascript/components/assignments/AssignmentDataAdmin.tsx @@ -100,7 +100,6 @@ export default function AssignmentDataAdmin(props) { useEffect(() => { if (suppressDirtyRef.current) { - suppressDirtyRef.current = false; return; } setDirty(DIRTY_STATUS.DIRTY); @@ -111,6 +110,10 @@ export default function AssignmentDataAdmin(props) { assignmentStartDate, assignmentEndDate, assignmentGroupOption, + assignmentFileSub, + assignmentLinkSub, + assignmentTextSub, + assignmentRubricId, assignmentGroupProjectId ]); @@ -144,6 +147,7 @@ export default function AssignmentDataAdmin(props) { ".json"; // Save + suppressDirtyRef.current = true; setSaveStatus(t("edit.status_saving")); axios({ url: url, @@ -171,7 +175,7 @@ export default function AssignmentDataAdmin(props) { setAssignmentData(data); setMessages( data.messages ); setDirty(DIRTY_STATUS.CLEAN); - navigate(`../${courseIdParam}/assignment/${assignmentId}`, { replace: true }); + navigate(`../${courseIdParam}/assignment/${data.assignment.id}`, { replace: true }); //getAssignmentData(); }) @@ -180,6 +184,7 @@ export default function AssignmentDataAdmin(props) { }) .finally(() => { dispatch(endTask("saving")); + suppressDirtyRef.current = false; }); }; @@ -237,15 +242,16 @@ export default function AssignmentDataAdmin(props) { return [{ id: -1, name: "no data" }]; }) .finally(() => { + suppressDirtyRef.current = false; dispatch(endTask()); }); }; - const save_btn = dirty ? ( + const saveBtn = ( }> ); const revCopyBtn = !notSubmitted ? (