Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 60 additions & 13 deletions app/javascript/components/AppStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,73 @@ 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 => {
const dirtyStatus = state.status.dirtyStatus as Record<string, boolean>;
for (const key in dirtyStatus) {
if (dirtyStatus[key]) {
return true;
}
}
return false;
});
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} />
<div style={{ display: "flex", justifyContent: "center", width: "100%", padding: "0.5rem 0 0.25rem", position: "relative", zIndex: 1100 }}>
<div
aria-live="polite"
role="status"
style={{
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
gap: "0.5rem",
padding: "0.35rem 0.75rem",
borderRadius: "999px",
border: `1px solid ${hasDirtyChanges ? "#f59e0b" : "#16a34a"}`,
backgroundColor: hasDirtyChanges ? "#fff7ed" : "#f0fdf4",
color: hasDirtyChanges ? "#b45309" : "#166534",
fontSize: "0.8rem",
fontWeight: 600,
lineHeight: 1.4,
width: "fit-content",
position: "relative",
zIndex: 1101
}}
>
<i className={`pi ${hasDirtyChanges ? "pi-exclamation-triangle" : "pi-check-circle"}`} />
<span>{hasDirtyChanges ? "Unsaved changes" : "Saved"}</span>
</div>
</div>
</>
);
}
13 changes: 10 additions & 3 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 } from "../infrastructure/StatusSlice";
import axios from "axios";
import { Editor } from "primereact/editor";
import EditorToolbar from "../toolbars/EditorToolbar";
Expand Down Expand Up @@ -42,6 +42,8 @@ export default function BingoGameDataAdmin(props) {
const { t, i18n } = useTranslation(`${category}s`);

const [dirty, setDirty] = useState(false);
const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [messages, setMessages] = useState({});
const [gameProjects, setGameProjects] = useState([
Expand Down Expand Up @@ -85,6 +87,10 @@ export default function BingoGameDataAdmin(props) {
}, [endpointStatus]);

useEffect(() => {
if (suppressDirtyRef.current) {
suppressDirtyRef.current = false;
return;
}
setDirty(true);
}, [
gameTopic,
Expand Down Expand Up @@ -161,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);
Expand Down Expand Up @@ -200,7 +207,7 @@ export default function BingoGameDataAdmin(props) {
};

const getBingoGameData = () => {
setDirty(true);
suppressDirtyRef.current = true;
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === bingoGameId) {
Expand Down
13 changes: 10 additions & 3 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,8 @@ import {
startTask,
endTask,
addMessage,
Priorities
Priorities,
useDirtyStatus
} from "../infrastructure/StatusSlice";
import { useTypedSelector } from "../infrastructure/AppReducers";
import axios from "axios";
Expand Down Expand Up @@ -39,7 +40,9 @@ export default function CandidateListEntry(props: Props) {
const { bingoGameId } = useParams();

const [dirty, setDirty] = useState(false);
const suppressDirtyRef = useRef(false);
const dispatch = useDispatch();
useDirtyStatus(category, dirty);

const [candidateListId, setCandidateListId] = useState(0);
const [topic, setTopic] = useState("");
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 Down Expand Up @@ -182,6 +185,10 @@ export default function CandidateListEntry(props: Props) {
}, [endpointStatus]);

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

Expand Down
3 changes: 2 additions & 1 deletion app/javascript/components/ConceptsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down
12 changes: 9 additions & 3 deletions app/javascript/components/assignments/AssignmentDataAdmin.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -44,6 +44,8 @@ export default function AssignmentDataAdmin(props) {
const navigate = useNavigate();

const [dirty, setDirty] = useState(false);
const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);
const [curTab, setCurTab] = useState(0);
const [assignmentProjects, setAssignmentProjects] = useState([
{ id: -1, name: "None Selected" }
Expand Down Expand Up @@ -98,6 +100,10 @@ export default function AssignmentDataAdmin(props) {
}, [endpointStatus]);

useEffect(() => {
if (suppressDirtyRef.current) {
suppressDirtyRef.current = false;
return;
}
setDirty(true);
}, [
assignmentName,
Expand Down Expand Up @@ -212,7 +218,7 @@ export default function AssignmentDataAdmin(props) {
setAssignmentRubricId(assignment.rubric_id || -1);
};
const getAssignmentData = () => {
setDirty(true);
suppressDirtyRef.current = true;
dispatch(startTask());
var url = endpoints.baseUrl + "/";
if (null === assignmentId) {
Expand Down
32 changes: 24 additions & 8 deletions app/javascript/components/assignments/AssignmentSubmission.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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";

//Redux store stuff
import { useDispatch } from "react-redux";
import { startTask, endTask } from "../infrastructure/StatusSlice";
import { startTask, endTask, addMessage, Priorities, useDirtyStatus } from "../infrastructure/StatusSlice";
import { IAssignment } from "./AssignmentViewer";

import { useTypedSelector } from "../infrastructure/AppReducers";
Expand Down Expand Up @@ -39,8 +40,11 @@ export default function AssignmentSubmission(props: Props) {
);

const dispatch = useDispatch();
const navigate = useNavigate();
const [t, i18n] = useTranslation(`${category}s`);
const [dirty, setDirty] = useState(false);
const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);

const [submissionId, setSubmissionId] = useState<string>();
const [updatedDate, setUpdatedDate] = useState<Temporal.ZonedDateTime | null>(null);
Expand All @@ -58,12 +62,15 @@ export default function AssignmentSubmission(props: Props) {
}, [endpointStatus, submissionId]);

useEffect(() => {
if (endpointStatus) {
setDirty(true);
if (suppressDirtyRef.current) {
suppressDirtyRef.current = false;
return;
}
setDirty(true);
}, [submissionTextEditor, submissionLink]);

const loadSubmission = () => {
suppressDirtyRef.current = true;
const url = props.rootPath === undefined
? `${endpoints.submissionUrl}${submissionId}.json`
: `/${props.rootPath}${endpoints.submissionUrl}${submissionId}.json`;
Expand Down Expand Up @@ -92,8 +99,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(() => {
Expand All @@ -112,7 +117,7 @@ export default function AssignmentSubmission(props: Props) {
value={submissionTextEditor}
headerTemplate={<EditorToolbar />}
onTextChange={e => {
setSubmissionTextEditor(e.htmlValue);
setSubmissionTextEditor(e.htmlValue || "");
}}
/>
</Col>
Expand Down Expand Up @@ -166,6 +171,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);
Expand All @@ -179,7 +190,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)
Expand Down
16 changes: 13 additions & 3 deletions app/javascript/components/checkin/InstallmentReport.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,7 +10,8 @@ import {
startTask,
endTask,
addMessage,
Priorities
Priorities,
useDirtyStatus
} from "../infrastructure/StatusSlice";
import { useTranslation } from "react-i18next";
import { useTypedSelector } from "../infrastructure/AppReducers";
Expand Down Expand Up @@ -79,6 +80,8 @@ export default function InstallmentReport(props: Props) {
const [contributions, setContributions] = useState({});
const [installment, setInstallment] = useState<IInstallmentState>({ comments: "" });
const [dirty, setDirty] = useState(false);
const suppressDirtyRef = useRef(false);
useDirtyStatus(category, dirty);

const [redirectState, setRedirectState] = useState(RedirectState.DECIDING);
const [redirectUrl, setRedirectUrl] = useState<string | undefined>(undefined);
Expand All @@ -98,7 +101,13 @@ export default function InstallmentReport(props: Props) {
setInstallment(inst);
};

useEffect(() => setDirty(true), [contributions, installment]);
useEffect(() => {
if (suppressDirtyRef.current) {
suppressDirtyRef.current = false;
return;
}
setDirty(true);
}, [contributions, installment]);

useEffect(() => {
if (endpointStatus) {
Expand All @@ -125,6 +134,7 @@ export default function InstallmentReport(props: Props) {

//Retrieve the latest data
const getContributions = () => {
suppressDirtyRef.current = true;
const url =
props.rootPath === undefined
? `${endpoints.baseUrl}${projectId}.json`
Expand Down
Loading