feat: add refresh buttons for exams and packets with loading state management
This commit is contained in:
@@ -1321,4 +1321,30 @@ h2:has(+ #exam-list:empty), h2:has(+ #packet-list:empty){
|
||||
to {
|
||||
box-shadow: 0 0 12px #F44336;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
background-color: transparent;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
color: #bbb;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
background-color: #333;
|
||||
border-color: #888;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.global-refresh-container {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
+64
-16
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xml:lang="en"
|
||||
@@ -26,7 +26,9 @@
|
||||
<div id="content">
|
||||
<div class="content-panel">
|
||||
<div class="nav-bar">
|
||||
<button id="btn-review" class="navigation nav-right"><b>☰</b><br/>OVERVIEW</button>
|
||||
<button id="btn-review" class="navigation nav-right">
|
||||
<b>☰</b><br />OVERVIEW
|
||||
</button>
|
||||
<button
|
||||
id="btn-submit-nav"
|
||||
class="submit-button navigation nav-right"
|
||||
@@ -51,7 +53,9 @@
|
||||
<div id="sync-status" class="nav-left"></div>
|
||||
<div id="timer" title="click to pause"></div>
|
||||
<div class="exam-name" title="currently loaded packet"></div>
|
||||
<button class="navigation nav-left" disabled="disabled"
|
||||
<button
|
||||
class="navigation nav-left"
|
||||
disabled="disabled"
|
||||
data-nav="previous"
|
||||
>
|
||||
<svg
|
||||
@@ -70,9 +74,7 @@
|
||||
<br />
|
||||
<span>PREVIOUS</span>
|
||||
</button>
|
||||
<button class="navigation nav-left"
|
||||
data-nav="next"
|
||||
>
|
||||
<button class="navigation nav-left" data-nav="next">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@@ -108,20 +110,46 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logout-background" role="dialog" aria-modal="true" aria-labelledby="finish-title">
|
||||
<div
|
||||
class="logout-background"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="finish-title"
|
||||
>
|
||||
<div class="finish-header">
|
||||
<div class="login-risr-box"><img class="login-risr-logo" draggable="false" alt="Logo" src="risr-assess-on-dark.png"></div>
|
||||
<div class="login-risr-box">
|
||||
<img
|
||||
class="login-risr-logo"
|
||||
draggable="false"
|
||||
alt="Logo"
|
||||
src="risr-assess-on-dark.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logout-panel finish-dialog" id="finish-dialog-panel">
|
||||
<div class="logout-title" id="finish-title">Ready to finish?</div>
|
||||
<div class="logout-error info">
|
||||
<p><strong>Answers are submitted automatically. You can complete the exam by clicking below.</strong></p>
|
||||
<p class="unanswered-line">You have <span id="unanswered-count">0</span> unanswered questions</p>
|
||||
<p>
|
||||
<strong
|
||||
>Answers are submitted automatically. You can complete the
|
||||
exam by clicking below.</strong
|
||||
>
|
||||
</p>
|
||||
<p class="unanswered-line">
|
||||
You have <span id="unanswered-count">0</span> unanswered
|
||||
questions
|
||||
</p>
|
||||
</div>
|
||||
<div class="logout-text">
|
||||
Click “Submit exam” before closing the tab or window
|
||||
</div>
|
||||
<div class="logout-text">Click “Submit exam” before closing the tab or window</div>
|
||||
<div class="logout-buttons">
|
||||
<button class="logout-button danger" id="button-logout">Submit exam</button>
|
||||
<button class="logout-button primary" id="button-continue">Continue exam</button>
|
||||
<button class="logout-button danger" id="button-logout">
|
||||
Submit exam
|
||||
</button>
|
||||
<button class="logout-button primary" id="button-continue">
|
||||
Continue exam
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,7 +165,7 @@
|
||||
id="btn-candidate-login"
|
||||
class="login-button"
|
||||
value="Enter candidate details"
|
||||
onclick="void(0)"
|
||||
onclick="void 0"
|
||||
/>
|
||||
<input
|
||||
type="button"
|
||||
@@ -150,14 +178,34 @@
|
||||
<div id="user" class="user-text"></div>
|
||||
</span>
|
||||
<div id="packets">
|
||||
<p>Click buttons below to load an exam / packet</p>
|
||||
<div class="global-refresh-container">
|
||||
<span id="load-exam-packet-instructions">
|
||||
<span>Click buttons below to load an exam / packet</span>
|
||||
</span>
|
||||
<button
|
||||
id="btn-refresh-all"
|
||||
class="refresh-button"
|
||||
title="Refresh all exams and packets"
|
||||
>
|
||||
Refresh All
|
||||
</button>
|
||||
</div>
|
||||
<span class="exam-wrapper">
|
||||
<h2>Exams:</h2>
|
||||
<span id="exam-list"></span>
|
||||
</span>
|
||||
|
||||
<span class="packet-wrapper">
|
||||
<h2>Packets:</h2>
|
||||
<h2>
|
||||
Packets:
|
||||
<button
|
||||
id="btn-refresh-packets"
|
||||
class="refresh-button"
|
||||
title="Refresh packets"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</h2>
|
||||
<span id="packet-list"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+173
-91
@@ -273,12 +273,29 @@ catch {
|
||||
*
|
||||
* if exam_query_url is defined in config.js this will be used in preference
|
||||
*/
|
||||
async function retrievePacketList() {
|
||||
let url = "packets/packets.json";
|
||||
function showLoadingState($btn, $targetContainer) {
|
||||
if (!$btn || !$btn.length) return null;
|
||||
let oldText = $btn.text();
|
||||
$btn.prop("disabled", true).text("...");
|
||||
if ($targetContainer && $targetContainer.length) {
|
||||
$targetContainer.css({
|
||||
"opacity": "0.4",
|
||||
"transition": "opacity 0.2s"
|
||||
});
|
||||
}
|
||||
return function restore(success = true) {
|
||||
$btn.prop("disabled", false).text(oldText);
|
||||
if ($targetContainer && $targetContainer.length) {
|
||||
$targetContainer.css("opacity", "1");
|
||||
if (success) {
|
||||
$targetContainer.fadeOut(100).fadeIn(100);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function retrievePacketList(target = "all", onComplete = null) {
|
||||
log.debug(`Load users from database...`)
|
||||
//let users = await user_db.user.toArray();
|
||||
//log.debug(`available users "${users}"`)
|
||||
let cid = localStorage.getItem("cid.cid")
|
||||
let passcode = localStorage.getItem("cid.passcode")
|
||||
|
||||
@@ -322,110 +339,147 @@ async function retrievePacketList() {
|
||||
} catch (e) {
|
||||
log.debug("Error parsing URL params for CID/passcode", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
log.debug("Try to set exam query url")
|
||||
if (URLS.exam_query_url != undefined) {
|
||||
|
||||
if (global_cid == null) {
|
||||
url = URLS.exam_query_url;
|
||||
} else {
|
||||
url = `${URLS.exam_query_url}/${global_cid}/${global_passcode}`;
|
||||
}
|
||||
}
|
||||
log.debug(`url: ${url}`)
|
||||
} catch (e) {
|
||||
log.debug("Unable to set exam query url", e)
|
||||
log.debug("this will default to packets/packets.json")
|
||||
}
|
||||
|
||||
if (url && (url.includes("exam/json") || url.endsWith("exam/json"))) {
|
||||
const types = ["anatomy", "rapid", "short", "long"];
|
||||
// Clear UI elements depending on target
|
||||
if (target === "all") {
|
||||
$("#packet-list").empty();
|
||||
$("#exam-list").empty();
|
||||
$("#cache-details summary").empty();
|
||||
$("#cache-details ul").empty();
|
||||
} else if (target === "exams") {
|
||||
$("#exam-list").empty();
|
||||
} else if (target === "packets") {
|
||||
$("#packet-list").empty();
|
||||
} else if (["anatomy", "rapid", "short", "long"].includes(target)) {
|
||||
$(`.exam-list.${target}`).find(".packet-button").remove();
|
||||
$(`.packet-list.${target}`).find(".packet-button").remove();
|
||||
}
|
||||
|
||||
types.forEach(type => {
|
||||
let typeUrl = url;
|
||||
if (url.includes("exam/json/unbased")) {
|
||||
typeUrl = url.replace("exam/json/unbased", `exam/json/${type}/unbased`);
|
||||
} else {
|
||||
typeUrl = url.replace("exam/json", `exam/json/${type}`);
|
||||
}
|
||||
let promises = [];
|
||||
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
url: typeUrl,
|
||||
success: function(data) {
|
||||
if (data.hasOwnProperty("exams")) {
|
||||
setUserInfo(data);
|
||||
|
||||
loadExamList(data, false);
|
||||
} else {
|
||||
loadPacketList(data);
|
||||
}
|
||||
},
|
||||
error: function(httpObj) {
|
||||
console.debug(httpObj);
|
||||
if (httpObj.status == 401 || httpObj.status == 404) {
|
||||
localStorage.removeItem("cid.cid");
|
||||
localStorage.removeItem("cid.passcode");
|
||||
$.notify("Unable to login", "error");
|
||||
$.notify("This page will now reload", "error");
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
$("#options-panel").show();
|
||||
$("#candidate-details").addClass("invalid-login");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
$.ajax({
|
||||
// 1. Fetch packets if target is "all" or "packets"
|
||||
if (target === "all" || target === "packets") {
|
||||
let packetsUrl = "packets/packets.json";
|
||||
let p = $.ajax({
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
url: url,
|
||||
url: packetsUrl,
|
||||
success: function(data) {
|
||||
if (data.hasOwnProperty("exams")) {
|
||||
loadExamList(data, true);
|
||||
} else {
|
||||
if (!data.hasOwnProperty("exams")) {
|
||||
loadPacketList(data);
|
||||
}
|
||||
},
|
||||
error: function(httpObj, textStatus) {
|
||||
console.debug(httpObj);
|
||||
if (httpObj.status == 401 || httpObj.status == 404) {
|
||||
localStorage.removeItem("cid.cid");
|
||||
localStorage.removeItem("cid.passcode");
|
||||
$.notify("Unable to login", "error");
|
||||
$.notify("This page will now reload", "error");
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
$("#options-panel").show();
|
||||
$("#candidate-details").addClass("invalid-login");
|
||||
}
|
||||
},
|
||||
})
|
||||
.done(function() {})
|
||||
.fail(function() {
|
||||
log.debug("Unable to load packets / exams, try loading '/packets' (legacy)")
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
log.debug("Unable to load packets, trying legacy '/packets'");
|
||||
$.getJSON("packets", function(data) {
|
||||
if (data.hasOwnProperty("exams")) {
|
||||
loadExamList(data, true);
|
||||
} else {
|
||||
if (!data.hasOwnProperty("exams")) {
|
||||
loadPacketList(data);
|
||||
}
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
console.warn("No packet list available");
|
||||
});
|
||||
}
|
||||
});
|
||||
promises.push(p);
|
||||
}
|
||||
|
||||
// 2. Fetch exams if target is "all" or "exams" or one of the types
|
||||
if (target === "all" || target === "exams" || ["anatomy", "rapid", "short", "long"].includes(target)) {
|
||||
let examUrl = null;
|
||||
try {
|
||||
if (URLS.exam_query_url != undefined) {
|
||||
if (global_cid == null || String(global_cid).startsWith("u-")) {
|
||||
examUrl = URLS.exam_query_url;
|
||||
} else {
|
||||
examUrl = `${URLS.exam_query_url}/${global_cid}/${global_passcode}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.debug("Unable to set exam query url", e);
|
||||
}
|
||||
|
||||
if (examUrl) {
|
||||
if (examUrl.includes("exam/json") || examUrl.endsWith("exam/json")) {
|
||||
const types = ["anatomy", "rapid", "short", "long"];
|
||||
const typesToFetch = types.includes(target) ? [target] : types;
|
||||
|
||||
typesToFetch.forEach(type => {
|
||||
let typeUrl = examUrl;
|
||||
if (examUrl.includes("exam/json/unbased")) {
|
||||
typeUrl = examUrl.replace("exam/json/unbased", `exam/json/${type}/unbased`);
|
||||
} else {
|
||||
typeUrl = examUrl.replace("exam/json", `exam/json/${type}`);
|
||||
}
|
||||
|
||||
let p = $.ajax({
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
url: typeUrl,
|
||||
success: function(data) {
|
||||
if (data.hasOwnProperty("exams")) {
|
||||
setUserInfo(data);
|
||||
loadExamList(data, false, target);
|
||||
}
|
||||
},
|
||||
error: function(httpObj) {
|
||||
console.debug(httpObj);
|
||||
if ((httpObj.status == 401 || httpObj.status == 404) && global_cid !== null && !String(global_cid).startsWith("u-")) {
|
||||
localStorage.removeItem("cid.cid");
|
||||
localStorage.removeItem("cid.passcode");
|
||||
$.notify("Unable to login", "error");
|
||||
$.notify("This page will now reload", "error");
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
$("#options-panel").show();
|
||||
$("#candidate-details").addClass("invalid-login");
|
||||
}
|
||||
}
|
||||
});
|
||||
promises.push(p);
|
||||
});
|
||||
} else {
|
||||
let p = $.ajax({
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
url: examUrl,
|
||||
success: function(data) {
|
||||
if (data.hasOwnProperty("exams")) {
|
||||
loadExamList(data, true, target);
|
||||
}
|
||||
},
|
||||
error: function(httpObj, textStatus) {
|
||||
console.debug(httpObj);
|
||||
if ((httpObj.status == 401 || httpObj.status == 404) && global_cid !== null && !String(global_cid).startsWith("u-")) {
|
||||
localStorage.removeItem("cid.cid");
|
||||
localStorage.removeItem("cid.passcode");
|
||||
$.notify("Unable to login", "error");
|
||||
$.notify("This page will now reload", "error");
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
$("#options-panel").show();
|
||||
$("#candidate-details").addClass("invalid-login");
|
||||
}
|
||||
}
|
||||
});
|
||||
promises.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (promises.length) {
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
if (onComplete) onComplete(true);
|
||||
})
|
||||
.always(function() {});
|
||||
.catch((err) => {
|
||||
console.warn("One or more requests failed", err);
|
||||
if (onComplete) onComplete(false);
|
||||
});
|
||||
} else {
|
||||
if (onComplete) onComplete(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +489,7 @@ async function retrievePacketList() {
|
||||
*
|
||||
* @param {JSON} data - json containing available exams
|
||||
*/
|
||||
async function loadExamList(data, clearExistingList = true) {
|
||||
async function loadExamList(data, clearExistingList = true, target = "all") {
|
||||
// If exam mode was enabled via URL param, hide packets by default
|
||||
if (typeof url_exam_mode !== "undefined" && url_exam_mode) {
|
||||
log.debug("URL exam mode active - hiding packets list");
|
||||
@@ -491,8 +545,12 @@ async function loadExamList(data, clearExistingList = true) {
|
||||
// enable invalidation when the question is updated on the server
|
||||
let exam_generated_map = {};
|
||||
if (clearExistingList) {
|
||||
$("#packet-list").empty();
|
||||
$("#exam-list").empty();
|
||||
if (target === "all" || target === "packets") {
|
||||
$("#packet-list").empty();
|
||||
}
|
||||
if (target === "all" || target === "exams") {
|
||||
$("#exam-list").empty();
|
||||
}
|
||||
$("#cache-details summary").empty();
|
||||
$("#cache-details ul").empty();
|
||||
}
|
||||
@@ -547,7 +605,11 @@ async function loadExamList(data, clearExistingList = true) {
|
||||
list = $(`.${target_list}.${exam.type}`);
|
||||
// or create and add it
|
||||
} else {
|
||||
list = $(`<div class='${target_list} ${exam.type}'><span class='packet-list-title'>${exam.type}</span><br/></div>`);
|
||||
let refreshBtnHtml = "";
|
||||
if (target_list === "exam-list") {
|
||||
refreshBtnHtml = ` <button class="refresh-button type-refresh-button" data-type="${exam.type}" title="Refresh ${exam.type} exams">Refresh</button>`;
|
||||
}
|
||||
list = $(`<div class='${target_list} ${exam.type}'><span class='packet-list-title'>${exam.type}${refreshBtnHtml}</span><br/></div>`);
|
||||
$(`#${target_list}`).append(list);
|
||||
|
||||
}
|
||||
@@ -2904,6 +2966,26 @@ $("#btn-user-login").click(function(evt) {
|
||||
//$(".exam-wrapper").toggle();
|
||||
});
|
||||
|
||||
$("#btn-refresh-all").on("click", function(e) {
|
||||
e.stopPropagation();
|
||||
let restore = showLoadingState($(this), $("#packets"));
|
||||
retrievePacketList("all", restore);
|
||||
});
|
||||
$("#btn-refresh-packets").on("click", function(e) {
|
||||
e.stopPropagation();
|
||||
let restore = showLoadingState($(this), $("#packet-list"));
|
||||
retrievePacketList("packets", restore);
|
||||
});
|
||||
|
||||
// Delegate click handlers for the dynamic per-exam-type refresh buttons
|
||||
$("#exam-list").on("click", ".type-refresh-button", function(e) {
|
||||
e.stopPropagation();
|
||||
let type = $(this).data("type");
|
||||
let $categoryContainers = $(`.exam-list.${type}, .packet-list.${type}`);
|
||||
let restore = showLoadingState($(this), $categoryContainers);
|
||||
retrievePacketList(type, restore);
|
||||
});
|
||||
|
||||
|
||||
|
||||
$("#btn-login").click(function(evt) {
|
||||
|
||||
Reference in New Issue
Block a user