Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a36cd13
Initial plan
Copilot Aug 30, 2026
7c6efa0
Fix assignment completion feedback and redirect UX
Copilot Aug 30, 2026
f1e759d
Apply remaining changes
Copilot Aug 30, 2026
1965a9c
Fix stale dirty-state initialization in form loaders
Copilot Aug 30, 2026
3809eac
Apply remaining changes
Copilot Aug 30, 2026
a5007f6
Tighten fresh-load dirty suppression for data-entry screens
Copilot Aug 30, 2026
c02d3e6
Remove premature dirty suppression reset and keep hydration guard in …
Copilot Aug 30, 2026
ee3e66f
Fix stale dirty state and status bar layering
Copilot Aug 30, 2026
e30d2bc
Merge branch 'main' into copilot/fix-completion-ui
mgmodell Sep 1, 2026
2f9eb59
in progress - need another dirty state
mgmodell Sep 10, 2026
ece20ea
merged
mgmodell Sep 10, 2026
291598c
version updates
mgmodell Sep 11, 2026
c90591b
useDirtyStatus hook is in progress
mgmodell Sep 13, 2026
c92fcb1
versions
mgmodell Sep 13, 2026
d4b7c53
useDirty has been implemented
mgmodell Sep 14, 2026
917de6d
versions
mgmodell Sep 14, 2026
8e4a51f
some fixes
mgmodell Sep 14, 2026
8bda931
fix for profile editing
mgmodell Sep 14, 2026
2dfac94
submit button shows properly now
mgmodell Sep 14, 2026
9812dde
versions
mgmodell Sep 15, 2026
0e21311
Button errors resolved.
mgmodell Sep 15, 2026
8f239f6
dirty works!
mgmodell Sep 16, 2026
cdb2740
version bumps
mgmodell Sep 16, 2026
f55d5cf
fix for dirty
mgmodell Sep 17, 2026
2ac5c29
moved and renamed the demo
mgmodell Sep 17, 2026
9585229
reducing wait time from 30s to 10s
mgmodell Sep 17, 2026
4ab29a1
bumping wait_for_render wait time up
mgmodell Sep 18, 2026
db05f7f
fix for concept tests.
mgmodell Sep 18, 2026
1a76093
versions
mgmodell Sep 20, 2026
36bcb1e
Fixed assignment flow.
mgmodell Sep 20, 2026
654df06
fixed the country and state point allocation
mgmodell Sep 21, 2026
2ab9e0c
versions and whatnot
mgmodell Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

source 'https://rubygems.org'
ruby '4.0.6'
ruby '4.0.7'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 8.1.3.1'
Expand Down Expand Up @@ -60,7 +60,7 @@ gem 'prawn'
gem 'prawn-qrcode'
gem 'prawn-table'
gem 'rails_12factor', group: :production
gem 'react_on_rails', '17.0.1'
gem 'react_on_rails', '17.1.0'
gem 'ruby-vips', '~> 2.0'
gem "solid_cable", "~> 4.0"
gem 'stopwords-filter2', require: 'stopwords'
Expand Down Expand Up @@ -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'
Expand Down
108 changes: 55 additions & 53 deletions Gemfile.lock

Large diffs are not rendered by default.

38 changes: 19 additions & 19 deletions app/javascript/components/ActivityLib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,95 +9,95 @@ export function iconForType(type: string) {
switch (type.toLowerCase()) {
case "home":
iconData.className = "home";
iconData.icon = <span className={`${iconData.className} pi pi-home`} />;
iconData.icon = <i className={`${iconData.className} pi pi-home`} />;
break;
case "welcome":
iconData.className = "welcome";
iconData.icon = <span className={`${iconData.className} pi pi-info-circle`} />;
iconData.icon = <i className={`${iconData.className} pi pi-info-circle`} />;
break;
case "profile":
iconData.className = "profile";
iconData.icon = <span className={`${iconData.className} pi pi-user`} />;
iconData.icon = <i className={`${iconData.className} pi pi-user`} />;
break;
case "perspective":
iconData.className = "perspective";
iconData.icon = <span className={`${iconData.className} pi pi-calculator`} />;
iconData.icon = <i className={`${iconData.className} pi pi-calculator`} />;
break;
case "concept":
case "concepts":
iconData.className = "concept";
iconData.icon = <span className={`${iconData.className} pi pi-tags`} />;
iconData.icon = <i className={`${iconData.className} pi pi-tags`} />;
break;
case "group experience":
case "experience":
case "experiences":
iconData.className = "experience";
iconData.icon = <span className={`${iconData.className} pi pi-wave-pulse`} />;
iconData.icon = <i className={`${iconData.className} pi pi-wave-pulse`} />;
break;
case "project":
case "assessment":
case "assessments":
iconData.className = "assessment";
iconData.icon = <span className={`${iconData.className} pi pi-sliders-h`} />;
iconData.icon = <i className={`${iconData.className} pi pi-sliders-h`} />;
break;
case "terms list":
case "bingo_game":
case "bingo games":
iconData.className = "bingo_game";
iconData.icon = <span className={`${iconData.className} pi pi-table`} />;
iconData.icon = <i className={`${iconData.className} pi pi-table`} />;
break;
case "group assignment":
case "assignment":
case "assignments":
iconData.className = "assignment";
iconData.icon = <span className={`${iconData.className} pi pi-file-edit`} />;
iconData.icon = <i className={`${iconData.className} pi pi-file-edit`} />;
break;
case "submission":
iconData.className = "submission";
iconData.icon = <span className={`${iconData.className} pi pi-file-export`} />;
iconData.icon = <i className={`${iconData.className} pi pi-file-export`} />;
break;
case "rubric":
case "rubrics":
iconData.className = "rubric";
iconData.icon = <span className={`${iconData.className} pi pi-table`} />;
iconData.icon = <i className={`${iconData.className} pi pi-table`} />;
break;
case "course":
case "courses":
iconData.className = "course";
iconData.icon = <span className={`${iconData.className} pi pi-book`} />;
iconData.icon = <i className={`${iconData.className} pi pi-book`} />;
break;
case "user":
case "users":
iconData.className = "user";
iconData.icon = <span className={`${iconData.className} pi pi-address-book`} />;
iconData.icon = <i className={`${iconData.className} pi pi-address-book`} />;
break;
case "reporting":
iconData.className = "reporting";
iconData.icon = <span className={`${iconData.className} pi pi-chart-bar`} />;
iconData.icon = <i className={`${iconData.className} pi pi-chart-bar`} />;
break;
case "administration":
case "admin":
iconData.className = "admin";
iconData.icon = <span className={`${iconData.className} pi pi-cog`} />;
iconData.icon = <i className={`${iconData.className} pi pi-cog`} />;
break;
case "school":
case "schools":
iconData.className = "school";
iconData.icon = <span className={`${iconData.className} pi pi-building-columns`} />;
iconData.icon = <i className={`${iconData.className} pi pi-building-columns`} />;
break;
case "consent_form":
case "consent_forms":
iconData.className = "consent_form";
iconData.icon = <span className={`${iconData.className} pi pi-file`} />;
iconData.icon = <i className={`${iconData.className} pi pi-file`} />;
break;
case 'demo':
case 'demonstration':
iconData.className = "demonstration";
iconData.icon = <span className={`${iconData.className} pi pi-video`} />;
iconData.icon = <i className={`${iconData.className} pi pi-video`} />;
break;
default:
iconData.className = `unit-${type}`;
iconData.icon = <span className={`${iconData.className} pi pi-asterisk`} />;
iconData.icon = <i className={`${iconData.className} pi pi-asterisk`} />;
}
return (
<>
Expand Down
38 changes: 25 additions & 13 deletions app/javascript/components/AppStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,38 @@ 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 dispatch = useDispatch();
const toast = React.useRef(null);
const toast = React.useRef<any>(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 <Toast ref={toast} />;
return (
<>
<Toast ref={toast} />
</>
);
}
27 changes: 16 additions & 11 deletions app/javascript/components/BingoBoards/BingoGameDataAdmin.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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, DIRTY_STATUS } from "../infrastructure/StatusSlice";
import axios from "axios";
import { Editor } from "primereact/editor";
import EditorToolbar from "../toolbars/EditorToolbar";
Expand Down Expand Up @@ -41,7 +41,8 @@ export default function BingoGameDataAdmin(props) {

const { t, i18n } = useTranslation(`${category}s`);

const [dirty, setDirty] = useState(false);
const [dirty, setDirty] = useDirtyStatus( bingoGameIdParam === "new" ? DIRTY_STATUS.DIRTY : DIRTY_STATUS.CLEAN );
const suppressDirtyRef = useRef(false);
const [curTab, setCurTab] = useState(0);
const [messages, setMessages] = useState({});
const [gameProjects, setGameProjects] = useState([
Expand Down Expand Up @@ -85,7 +86,10 @@ export default function BingoGameDataAdmin(props) {
}, [endpointStatus]);

useEffect(() => {
setDirty(true);
if (suppressDirtyRef.current || null === bingoGameId ) {
return;
}
setDirty(DIRTY_STATUS.DIRTY);
}, [
gameTopic,
gameDescriptionEditor,
Expand Down Expand Up @@ -161,9 +165,8 @@ export default function BingoGameDataAdmin(props) {
setGameGroupDiscount(bingo_game.group_discount || 0);
setGameGroupProjectId(bingo_game.project_id);
setFoundWords(data.found_words);
setDirty(DIRTY_STATUS.CLEAN);

//getBingoGameData();
//setDirty(false);
navigate(`../${courseIdParam}/bingo_game/${bingoGameId}`, {
replace: true
});
Expand All @@ -173,6 +176,7 @@ export default function BingoGameDataAdmin(props) {
})
.finally(() => {
dispatch(endTask("saving"));
suppressDirtyRef.current = false;
});
};

Expand Down Expand Up @@ -200,7 +204,7 @@ export default function BingoGameDataAdmin(props) {
};

const getBingoGameData = () => {
setDirty(true);
suppressDirtyRef.current = true;
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === bingoGameId) {
Expand Down Expand Up @@ -240,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());
});
};
Expand All @@ -258,11 +263,11 @@ export default function BingoGameDataAdmin(props) {
return deadline;
}, [gameEndDate, gameLeadTime]);

const save_btn = dirty ? (
const save_btn = (
<Suspense fallback={<Skeleton className="mb-2" />}>
<Button
color="primary"
//className={classes["button"]}
disabled={dirty !== DIRTY_STATUS.DIRTY && bingoGameId}
onClick={() => {
saveBingoGame();
}}
Expand All @@ -272,7 +277,7 @@ export default function BingoGameDataAdmin(props) {
{null == bingoGameId ? t("create_bingo_btn") : t("update_bingo_btn")}
</Button>
</Suspense>
) : null;
);

const group_options = gameGroupOption ? (
<Suspense fallback={<Skeleton className="mb-2" />}>
Expand Down
23 changes: 15 additions & 8 deletions app/javascript/components/BingoBoards/CandidateListEntry.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,7 +10,9 @@ import {
startTask,
endTask,
addMessage,
Priorities
Priorities,
useDirtyStatus,
DIRTY_STATUS
} from "../infrastructure/StatusSlice";
import { useTypedSelector } from "../infrastructure/AppReducers";
import axios from "axios";
Expand Down Expand Up @@ -38,7 +40,8 @@ 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();

const [candidateListId, setCandidateListId] = useState(0);
Expand All @@ -56,8 +59,8 @@ export default function CandidateListEntry(props: Props) {
const [requestCollaborationUrl, setRequestCollaborationUrl] = useState("");

const getCandidateList = () => {
suppressDirtyRef.current = true;
dispatch(startTask());
setDirty(true);
const url =
props.rootPath === undefined
? `${endpoints.baseUrl}${bingoGameId}.json`
Expand All @@ -81,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);
Expand Down Expand Up @@ -159,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 => {
Expand All @@ -182,7 +185,11 @@ export default function CandidateListEntry(props: Props) {
}, [endpointStatus]);

useEffect(() => {
setDirty(true);
if (suppressDirtyRef.current) {
suppressDirtyRef.current = false;
return;
}
setDirty(DIRTY_STATUS.DIRTY);
}, [candidates]);

// TODO: Fix the check to see if the form is dirty
Expand All @@ -209,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);
Expand Down
Loading
Loading