Skip to content
Merged
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
7 changes: 4 additions & 3 deletions docs/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ <h2 id="publishing">Publishing to Canvas</h2>
<p>Each editor&rsquo;s footer says where the item stands and shows the one button you need next:</p>
<ul>
<li><b>Not in Canvas yet</b>: <b>Add to Canvas</b> creates the item in Canvas, unpublished, with its rendered description, dates, rubric and module entry. A module that does not exist yet is created.</li>
<li><b>3 differences from Canvas</b> (in amber): <b>Update in Canvas</b> pushes the current file over the existing Canvas item. Submissions and grades are untouched. Click the amber text to open <a href="#compare">Compare with Canvas</a>.</li>
<li><b>3 differences from Canvas</b> (in amber): <b>Update in Canvas</b> pushes the current file over the existing Canvas item, and moves it into the module its file is in if you moved the file, taking it out of any other module. Submissions and grades are untouched. Click the amber text to open <a href="#compare">Compare with Canvas</a>.</li>
<li><b>In sync &middot; students can&rsquo;t see it yet</b>: <b>Publish in Canvas</b> makes it visible to students. If the item&rsquo;s module is still unpublished in Canvas, the app says so and links to the modules page; it does not publish the module for you.</li>
<li><b>In sync &middot; Published</b>: nothing to do; <b>View in Canvas</b> opens it.</li>
<li>For a <b>quiz</b>, Update in Canvas also replaces the questions with the file&rsquo;s when any of them differ. If students have already started, it asks first: their attempts keep the questions they got and are not regraded. It refuses a change to the quiz&rsquo;s point total, since their scores are out of the total they were given.</li>
Expand All @@ -308,8 +308,8 @@ <h2 id="publishing">Publishing to Canvas</h2>
<h3 id="compare">Compare with Canvas</h3>
<p>When an item&rsquo;s border is red, <b>Compare with Canvas</b> (the amber <em>differences from Canvas</em> text in the editor footer, the menu on the footer&rsquo;s main button, the right-click menu, and the red pill&rsquo;s hover card) opens a page that lists every difference between your file and Canvas:</p>
<ul>
<li>A settings table with the file&rsquo;s value and Canvas&rsquo;s side by side: dates, assignment group, group set, points and each rubric row, and for a quiz its password, attempts and other options.</li>
<li>For a <b>quiz</b>, a row for every question, compared on type, points, text, answers (correct ones marked &#10003;) and feedback. Questions are lined up by what they say, not by number, so deleting or adding one doesn&rsquo;t make every later question look changed: a question you deleted from the file shows as one <em>Canvas question N</em> row, one you added as a row with nothing on the Canvas side, and one you moved as a <em>position</em> difference. Every question shows in full on both sides, answers included, and &#8800; marks the lines of each part that differs. The quiz editor&rsquo;s footer counts these differences too. The calendar checks questions for this week&rsquo;s quizzes and the next three weeks&rsquo;, fetching them in the background one quiz at a time (this week first) so Canvas doesn&rsquo;t throttle you; a quiz outside those four weeks gets its red border from settings and description only.</li>
<li>A settings table with the file&rsquo;s value and Canvas&rsquo;s side by side: the modules (Canvas should have the item only in the module its file is in), dates, assignment group, group set, points and each rubric row, and for a quiz its password, attempts and other options.</li>
<li>For a <b>quiz</b>, a row for every question, compared on type, points, text, answers (correct ones marked &#10003;) and feedback. Questions are lined up by what they say, not by number, so deleting or adding one doesn&rsquo;t make every later question look changed: a question you deleted from the file shows as one <em>Canvas question N</em> row, one you added as a row with nothing on the Canvas side, and one you moved as a <em>position</em> difference. Every question shows in full on both sides, answers included, and &#8800; marks the lines of each part that differs. The quiz editor&rsquo;s footer counts these differences too. The calendar checks questions for this week&rsquo;s quizzes and the next three weeks&rsquo;, fetching them in the background one quiz at a time (this week first) so Canvas doesn&rsquo;t throttle you; a quiz outside those four weeks gets its red border from its module, settings and description only.</li>
<li>For a quiz students have started, a <b>What students got</b> section for each quiz version they took, comparing the file with the questions from one of its attempts (ignoring each attempt&rsquo;s own answer shuffle). Canvas&rsquo;s current questions can differ from these until <em>Save it now</em>, and this is the only way to see what students actually got.</li>
<li>The description (a page&rsquo;s content) <em>side by side</em>, as <em>changes only</em> (a word-level diff), or as the normalized <em>HTML</em> when you need to see why two descriptions that look the same count as different.</li>
<li>An <b>Update Canvas</b> button at the bottom that pushes the file, with a note saying exactly what it will change. The top of the page says when it last read Canvas, and <em>refresh</em> reads it again.</li>
Expand Down Expand Up @@ -364,6 +364,7 @@ <h2 id="tips">Tips</h2>
<h2 id="changes">Recent changes</h2>
<div class="tw"><table>
<tr><th>When</th><th>What changed</th></tr>
<tr><td style="white-space:nowrap">Oct 2026</td><td><a href="#publishing">Update in Canvas</a> moves an item to the module its file is now in, or adds it to that module if it was in none, and takes it out of any other module. <a href="#compare">Compare with Canvas</a>, the editor footer and the calendar count a moved file, or an item in more than one Canvas module, as a difference.</td></tr>
<tr><td style="white-space:nowrap">Oct 2026</td><td>Opening a course whose term has ended no longer shows a <em>roster.groupSets &hellip; 403</em> error: Canvas won&rsquo;t list group sets for a finished course, so it shows none.</td></tr>
<tr><td style="white-space:nowrap">Oct 2026</td><td><a href="#compare">Compare with Canvas</a> checks a quiz&rsquo;s password, allowed attempts, shuffle answers, show correct answers and one question at a time, so changing only one of them no longer shows the quiz as in sync.</td></tr>
<tr><td style="white-space:nowrap">Oct 2026</td><td><a href="#compare">Compare with Canvas</a> compares short-answer answers as plain text, so an answer like <code>+</code> or <code>a*b*c</code> no longer shows as different when it matches.</td></tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface RosterForStatus {
}

export type SyncSection =
| "modules"
| "dates"
| "grading"
| "groups"
Expand Down Expand Up @@ -677,6 +678,26 @@ function buildReport(
};
}

/**
* The file's module folder against the Canvas modules holding the item: a
* file is in one folder, so Canvas should have the item in that module only.
*/
function checkModules(file: string, canvasModules: string[]): SyncField {
const canvas = [...new Set(canvasModules)].sort();
return field({
key: "modules",
label: "Modules",
section: "modules",
local: file,
canvas: canvas.join(", ") || UNSET,
same: canvas.length === 1 && canvas[0] === file,
message:
canvas.length > 0
? `modules different: ${file} vs ${canvas.join(", ")}`
: "not in a module in canvas",
});
}

/**
* Compares a local item with its Canvas copy field by field. `status` and
* `message` drive the calendar border and tooltip; `fields` and `description`
Expand All @@ -690,6 +711,7 @@ export function getSyncReport({
canvasLinkTargets,
roster,
canvasQuestions,
module,
}: {
item: LocalQuiz | LocalAssignment | LocalCoursePage;
canvasItem: CanvasQuiz | CanvasAssignment | CanvasPage | undefined;
Expand All @@ -699,6 +721,8 @@ export function getSyncReport({
roster?: RosterForStatus;
/** A quiz's questions in Canvas; without them only its settings are compared. */
canvasQuestions?: CanvasQuizQuestion[];
/** The file's module and the Canvas modules holding the item; without them the module is not compared. */
module?: { file: string; canvas: string[] };
}): SyncReport {
if (!canvasItem)
return {
Expand All @@ -708,7 +732,9 @@ export function getSyncReport({
fields: [],
};

const fields: SyncField[] = [];
const fields: SyncField[] = module
? [checkModules(module.file, module.canvas)]
: [];

if (type === "page") {
const page = item as LocalCoursePage;
Expand Down
43 changes: 0 additions & 43 deletions src/app/course/[courseName]/calendar/day/getStatus.tsx

This file was deleted.

44 changes: 44 additions & 0 deletions src/app/course/[courseName]/calendar/day/getSyncStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,3 +534,47 @@ describe("getSyncReport - quiz options", () => {
).toEqual([]);
});
});

describe("getSyncReport - module", () => {
const report = (canvasModules?: string[]) =>
getSyncReport({
item: baseLocalAssignment,
canvasItem: baseCanvasAssignment,
type: "assignment",
settings: baseSettings,
module: canvasModules && { file: "Week 2", canvas: canvasModules },
});

it("matches when the Canvas item is only in the file's module", () => {
expect(report(["Week 2"]).differences).toEqual([]);
});

it("differs when the Canvas item is also in another module", () => {
expect(report(["Week 2", "Week 1"]).differences[0]).toMatchObject({
label: "Modules",
canvas: "Week 1, Week 2",
message: "modules different: Week 2 vs Week 1, Week 2",
});
});

it("differs when the file moved to another module", () => {
const [difference] = report(["Week 1"]).differences;
expect(difference).toMatchObject({
key: "modules",
local: "Week 2",
canvas: "Week 1",
message: "modules different: Week 2 vs Week 1",
});
});

it("differs when the Canvas item is in no module", () => {
expect(report([]).differences[0]).toMatchObject({
canvas: "—",
message: "not in a module in canvas",
});
});

it("is not compared until the Canvas modules load", () => {
expect(report(undefined).fields.map((f) => f.key)).not.toContain("modules");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export const AssignmentDayItemContextMenu: FC<{
updateInCanvasMutation.mutate({
canvasAssignmentId: assignmentInCanvas.id,
assignment: item as LocalAssignment,
moduleName,
});
handleClose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const QuizDayItemContextMenu: FC<{
updateInCanvasMutation.mutate({
quiz: item as LocalQuiz,
canvasQuizId: quizInCanvas.id,
moduleName,
});
handleClose();
};
Expand Down
27 changes: 22 additions & 5 deletions src/app/course/[courseName]/calendar/day/useTodaysItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
} from "@/features/local/utils/timeUtils";
import { ReactNode } from "react";
import { useCalendarItemsContext } from "../../context/calendarItemsContext";
import { getStatus } from "./getStatus";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useCanvasAssignmentsQuery } from "@/features/canvas/hooks/canvasAssignmentHooks";
import { useCanvasPagesQuery } from "@/features/canvas/hooks/canvasPageHooks";
Expand All @@ -16,7 +15,13 @@ import {
useRosterGroupSetsQuery,
useRosterStudentsQuery,
} from "@/features/canvas/roster/rosterHooks";
import { getScheduleEntryStatus, SyncField } from "./getAssignmentSyncStatus";
import {
getScheduleEntryStatus,
getSyncStatus,
SyncField,
} from "./getAssignmentSyncStatus";
import { useCanvasModulesWithItemsQuery } from "@/features/canvas/hooks/canvasModuleHooks";
import { canvasModuleNames } from "@/features/canvas/services/canvasModuleService";
import { AssignmentScheduleEntry } from "@/features/local/assignments/models/localAssignment";
import { useCalendarQuizQuestions } from "../../context/CalendarQuizQuestionsProvider";

Expand Down Expand Up @@ -52,6 +57,15 @@ export function useTodaysItems(day: string) {
const roster = { students: rosterStudents, groupSets: rosterGroupSets };
// filled in the background for this week's and the next three weeks' quizzes
const quizQuestions = useCalendarQuizQuestions();
const { data: canvasModules } = useCanvasModulesWithItemsQuery();
const module = (
file: string,
type: TodayItem["type"],
canvasItem: { id?: number; url?: string } | undefined,
) =>
canvasModules && canvasItem
? { file, canvas: canvasModuleNames(canvasModules, type, canvasItem) }
: undefined;

const assignments: TodayItem[] = todaysModules
? Object.keys(todaysModules).flatMap((moduleName) =>
Expand All @@ -63,7 +77,7 @@ export function useTodaysItems(day: string) {
type: "assignment" as const,
item: assignment,
moduleName,
...getStatus({
...getSyncStatus({
item: assignment,
canvasItem: canvasAssignment,
type: "assignment",
Expand All @@ -73,6 +87,7 @@ export function useTodaysItems(day: string) {
quizzes: canvasQuizzes,
},
roster,
module: module(moduleName, "assignment", canvasAssignment),
}),
};
}),
Expand Down Expand Up @@ -111,12 +126,13 @@ export function useTodaysItems(day: string) {
type: "quiz" as const,
item: quiz,
moduleName,
...getStatus({
...getSyncStatus({
item: quiz,
canvasItem: canvasQuiz,
type: "quiz",
settings,
canvasQuestions: quizQuestions[quiz.name],
module: module(moduleName, "quiz", canvasQuiz),
}),
};
}),
Expand All @@ -131,11 +147,12 @@ export function useTodaysItems(day: string) {
type: "page" as const,
item: page,
moduleName,
...getStatus({
...getSyncStatus({
item: page,
canvasItem: canvasPage,
type: "page",
settings,
module: module(moduleName, "page", canvasPage),
}),
};
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
EditorFooter,
FooterAction,
} from "@/app/course/[courseName]/EditorFooter";
import { useCanvasModuleNames } from "@/features/canvas/hooks/canvasModuleHooks";
import { getSyncReport } from "@/app/course/[courseName]/calendar/day/getAssignmentSyncStatus";
import { ClassroomIcon, TrashIcon } from "@/components/icons/ActionIcons";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
Expand Down Expand Up @@ -71,6 +72,10 @@ export function AssignmentFooterButtons({
const assignmentInCanvas = canvasAssignments?.find(
(a) => a.name === assignmentName,
);
const { names: canvasModules } = useCanvasModuleNames(
"assignment",
assignmentInCanvas,
);

const anythingIsLoading =
addToCanvas.isPending ||
Expand All @@ -92,13 +97,16 @@ export function AssignmentFooterButtons({
quizzes: canvasQuizzes,
},
roster: { students: rosterStudents, groupSets: rosterGroupSets },
module: canvasModules && { file: moduleName, canvas: canvasModules },
}).differences
: [],
[
assignment,
assignmentInCanvas,
canvasAssignments,
canvasModules,
canvasQuizzes,
moduleName,
rosterGroupSets,
rosterStudents,
settings,
Expand Down Expand Up @@ -159,6 +167,7 @@ export function AssignmentFooterButtons({
updateAssignment.mutate({
canvasAssignmentId: assignmentInCanvas.id,
assignment,
moduleName,
})
}
onViewInCanvas={() => {
Expand Down
Loading