mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 22:07:18 +01:00
fixed todolist not marking complete bug
This commit is contained in:
parent
6850b3cb4f
commit
20430ba343
125
js/content.js
125
js/content.js
@ -4653,6 +4653,11 @@ function markAs(item, element, makeComplete) {
|
|||||||
const csrfToken = CSRFtoken();
|
const csrfToken = CSRFtoken();
|
||||||
const completeState = makeComplete;
|
const completeState = makeComplete;
|
||||||
|
|
||||||
|
// Pre-click state, captured so the optimistic UI can be rolled back if
|
||||||
|
// the server definitively refuses the change (see revertOptimisticUI).
|
||||||
|
const previousComplete = item.planner_override ? item.planner_override.marked_complete === true : false;
|
||||||
|
const hadPin = isPinnedIncomplete(item);
|
||||||
|
|
||||||
// Checking an item off clears any local incomplete pin; unchecking a
|
// Checking an item off clears any local incomplete pin; unchecking a
|
||||||
// genuinely submitted item sets one, since the submission itself can't
|
// genuinely submitted item sets one, since the submission itself can't
|
||||||
// be undone server-side.
|
// be undone server-side.
|
||||||
@ -4687,8 +4692,9 @@ function markAs(item, element, makeComplete) {
|
|||||||
element.style.opacity = "0";
|
element.style.opacity = "0";
|
||||||
|
|
||||||
// update progress rings immediately so they animate while the item slides/fades
|
// update progress rings immediately so they animate while the item slides/fades
|
||||||
|
const refreshProgressRings = () => {
|
||||||
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
|
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
|
||||||
if (progressPlaceholder && typeof assignments?.then === 'function' && progressRingsEnabled()) {
|
if (!progressPlaceholder || typeof assignments?.then !== 'function' || !progressRingsEnabled()) return;
|
||||||
assignments.then(data => {
|
assignments.then(data => {
|
||||||
const courseId = getCurrentCourseId();
|
const courseId = getCurrentCourseId();
|
||||||
const scopedData = getTodoScopedData(data.map(d => Object.assign({}, d)), courseId);
|
const scopedData = getTodoScopedData(data.map(d => Object.assign({}, d)), courseId);
|
||||||
@ -4708,7 +4714,36 @@ function markAs(item, element, makeComplete) {
|
|||||||
|
|
||||||
renderProgressRings(progressPlaceholder, scopedData);
|
renderProgressRings(progressPlaceholder, scopedData);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
refreshProgressRings();
|
||||||
|
|
||||||
|
// --- Failure rollback ---
|
||||||
|
// Restores the pre-click state when the server definitively refuses the
|
||||||
|
// toggle: a POST 400 whose existing override can't be recovered, a failed
|
||||||
|
// PUT, or a network error. Canvas's planner_override uniqueness scope is
|
||||||
|
// (user, plannable_type, plannable_id) regardless of workflow_state, but
|
||||||
|
// soft-deleted override rows are invisible to BOTH read endpoints, so a
|
||||||
|
// dormant row can permanently block the POST with 400 "already exists".
|
||||||
|
// Without a rollback the optimistic UI would silently lie until the next
|
||||||
|
// reload, when the item reappears unchecked — the reported "cannot check
|
||||||
|
// off / won't move to Completed" bug.
|
||||||
|
const revertOptimisticUI = (reason) => {
|
||||||
|
item.planner_override = item.planner_override || {};
|
||||||
|
item.planner_override.marked_complete = previousComplete;
|
||||||
|
// undo the pin side effects of this toggle
|
||||||
|
if (completeState) {
|
||||||
|
if (hadPin) setPinnedIncomplete(item, true);
|
||||||
|
} else if (item.submissions?.submitted && !hadPin) {
|
||||||
|
setPinnedIncomplete(item, false);
|
||||||
}
|
}
|
||||||
|
console.warn("planner override not persisted, reverting UI:", reason);
|
||||||
|
// snap the card back if it is still in the DOM (before the 400ms swap)
|
||||||
|
element.style.transform = "";
|
||||||
|
element.style.opacity = "";
|
||||||
|
const loc = document.querySelector("#canvasrefined-todo-list");
|
||||||
|
if (loc) { clearTodoList(); createTodoSections(loc); }
|
||||||
|
refreshProgressRings();
|
||||||
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearTodoList();
|
clearTodoList();
|
||||||
@ -4720,9 +4755,9 @@ function markAs(item, element, makeComplete) {
|
|||||||
// successful POST/PUT is stored on the in-memory item so subsequent
|
// successful POST/PUT is stored on the in-memory item so subsequent
|
||||||
// toggles PUT (update) instead of POSTing a duplicate. If a POST still
|
// toggles PUT (update) instead of POSTing a duplicate. If a POST still
|
||||||
// collides with an existing override (400 "already exists"), we look up
|
// collides with an existing override (400 "already exists"), we look up
|
||||||
// the real id and retry with a PUT. Only if all of that fails is the
|
// the real id and retry with a PUT. If every attempt fails, the
|
||||||
// response treated as a soft success: the UI already reflects the
|
// optimistic UI is rolled back so the card visibly returns instead of
|
||||||
// intended state, so we just log it.
|
// silently dropping the check-off on the next reload.
|
||||||
const sendOverride = (override) => fetch(domain + "/api/v1/planner/overrides" + (override && override.id ? "/" + override.id : ""), {
|
const sendOverride = (override) => fetch(domain + "/api/v1/planner/overrides" + (override && override.id ? "/" + override.id : ""), {
|
||||||
method: override && override.id ? "PUT" : "POST",
|
method: override && override.id ? "PUT" : "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -4738,6 +4773,46 @@ function markAs(item, element, makeComplete) {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Loads every planner override the user has. The index endpoint is
|
||||||
|
// paginated at 10 items per page and only ever returns ACTIVE overrides —
|
||||||
|
// Canvas soft-deletes overrides (workflow_state="deleted") instead of
|
||||||
|
// removing the row, and those dormant rows still satisfy the
|
||||||
|
// (user, plannable_type, plannable_id) uniqueness constraint. A dormant
|
||||||
|
// row therefore makes POST create fail with 400 "already exists" while
|
||||||
|
// being unlistable, which is why items in some courses could never be
|
||||||
|
// checked off: the old recovery read a single un-paginated page, missed
|
||||||
|
// the row, and never PUT. Page through with per_page=100 following the
|
||||||
|
// Link header (same pattern as the planner items fetch). Returns null on
|
||||||
|
// any failure so the caller can distinguish "no match" from "unknown".
|
||||||
|
const fetchAllOverrides = async () => {
|
||||||
|
const all = [];
|
||||||
|
let url = domain + "/api/v1/planner/overrides?per_page=100";
|
||||||
|
for (let page = 0; page < 10 && url; page++) {
|
||||||
|
const pageResp = await fetch(url, { headers: { "accept": "application/json" } });
|
||||||
|
if (!pageResp.ok) return null;
|
||||||
|
const data = await pageResp.json();
|
||||||
|
if (!Array.isArray(data)) return null;
|
||||||
|
all.push(...data);
|
||||||
|
url = getNextPageUrl(pageResp.headers.get("Link"));
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A stored override may not carry the identity the planner items feed
|
||||||
|
// gave us: Canvas re-points overrides on save (discussion overrides move
|
||||||
|
// to the root topic id, assignment overrides can be re-typed to
|
||||||
|
// quiz/discussion_topic/wiki_page with a different id), so also match on
|
||||||
|
// the associated assignment_id as a fallback.
|
||||||
|
const matchesItem = (o) => {
|
||||||
|
if (!o) return false;
|
||||||
|
if (String(o.plannable_id) === String(item.plannable_id) &&
|
||||||
|
String(o.plannable_type).toLowerCase() === String(item.plannable_type).toLowerCase()) return true;
|
||||||
|
const assignmentId = item.plannable && item.plannable.assignment_id != null
|
||||||
|
? item.plannable.assignment_id
|
||||||
|
: (item.plannable_type === "assignment" ? item.plannable_id : null);
|
||||||
|
return assignmentId != null && o.assignment_id != null && String(o.assignment_id) === String(assignmentId);
|
||||||
|
};
|
||||||
|
|
||||||
sendOverride(item.planner_override)
|
sendOverride(item.planner_override)
|
||||||
.then(async resp => {
|
.then(async resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@ -4758,38 +4833,44 @@ function markAs(item, element, makeComplete) {
|
|||||||
}
|
}
|
||||||
// A POST can return 400 because an override already exists server-side
|
// A POST can return 400 because an override already exists server-side
|
||||||
// under an id we never learned (e.g. it was created earlier without the
|
// under an id we never learned (e.g. it was created earlier without the
|
||||||
// response being captured). Look up the user's overrides, find the one
|
// response being captured, or a dormant soft-deleted row blocks it).
|
||||||
// for this item, and retry with a PUT so the toggle actually persists.
|
// Look up the user's overrides, find the one for this item, and retry
|
||||||
|
// with a PUT so the toggle actually persists.
|
||||||
if (!(item.planner_override && item.planner_override.id)) {
|
if (!(item.planner_override && item.planner_override.id)) {
|
||||||
try {
|
try {
|
||||||
const listResp = await fetch(domain + "/api/v1/planner/overrides", {
|
const overrides = await fetchAllOverrides();
|
||||||
headers: { "accept": "application/json" }
|
const match = overrides && overrides.find(matchesItem);
|
||||||
});
|
|
||||||
if (listResp.ok) {
|
|
||||||
const overrides = await listResp.json();
|
|
||||||
const match = (Array.isArray(overrides) ? overrides : []).find(o =>
|
|
||||||
o &&
|
|
||||||
String(o.plannable_id) === String(item.plannable_id) &&
|
|
||||||
String(o.plannable_type).toLowerCase() === String(item.plannable_type).toLowerCase());
|
|
||||||
if (match && match.id) {
|
if (match && match.id) {
|
||||||
item.planner_override = item.planner_override || {};
|
item.planner_override = item.planner_override || {};
|
||||||
item.planner_override.id = match.id;
|
item.planner_override.id = match.id;
|
||||||
const retry = await sendOverride(item.planner_override);
|
const retry = await sendOverride(item.planner_override);
|
||||||
if (!retry.ok) {
|
if (retry.ok) {
|
||||||
console.warn("planner override PUT retry returned", retry.status);
|
try {
|
||||||
}
|
const saved = await retry.json();
|
||||||
|
if (saved && saved.id) item.planner_override.id = saved.id;
|
||||||
|
} catch (e) { /* id stays unknown */ }
|
||||||
|
console.log("marked as complete (recovered override id)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.warn("planner override PUT retry returned", retry.status);
|
||||||
|
return revertOptimisticUI("PUT retry returned " + retry.status);
|
||||||
}
|
}
|
||||||
|
// No matching override (or the listing failed): nothing to
|
||||||
|
// PUT against, so the change was never persisted.
|
||||||
|
return revertOptimisticUI("POST returned " + resp.status + " and no matching override was found");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error recovering planner override id", e);
|
console.error("error recovering planner override id", e);
|
||||||
|
return revertOptimisticUI("override recovery failed: " + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Still non-OK with nothing to recover: logged but not surfaced, the
|
// A PUT on a known id failed (e.g. the override was removed
|
||||||
// UI already reflects the intended state.
|
// server-side between our listing and the toggle): roll back.
|
||||||
console.warn("planner override request returned", resp.status, "— UI already updated optimistically");
|
return revertOptimisticUI("request returned " + resp.status);
|
||||||
})
|
})
|
||||||
.catch(err => console.error("error marking as complete", err));
|
.catch(err => {
|
||||||
|
console.error("error marking as complete", err);
|
||||||
|
revertOptimisticUI(err);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Canvas Refined",
|
"name": "Canvas Refined",
|
||||||
"description": "Even More Feature packed extension for Canvas.",
|
"description": "Even More Feature packed extension for Canvas.",
|
||||||
"version": "7.1.0",
|
"version": "7.1.1",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icon/icon-16.png",
|
"16": "icon/icon-16.png",
|
||||||
"32": "icon/icon-32.png",
|
"32": "icon/icon-32.png",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user