diff --git a/bases/rsptx/assignment_server_api/assignment_builder/package-lock.json b/bases/rsptx/assignment_server_api/assignment_builder/package-lock.json
index c4621c027..3a58d7779 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/package-lock.json
+++ b/bases/rsptx/assignment_server_api/assignment_builder/package-lock.json
@@ -68,13 +68,13 @@
"reselect": "^5.1.1",
"screenfull": "^6.0.2",
"tippy.js": "^6.3.7",
- "tiptap-extension-code-block-prism": "^0.0.2-e",
+ "tiptap-extension-code-block-prism": "0.0.2-e",
"typesafe-actions": "^5.1.0",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.3",
- "@playwright/test": "^1.60.0",
+ "@playwright/test": "1.61.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
@@ -3394,13 +3394,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
- "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz",
+ "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.60.0"
+ "playwright": "1.61.0"
},
"bin": {
"playwright": "cli.js"
@@ -10419,13 +10419,13 @@
}
},
"node_modules/playwright": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
- "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz",
+ "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.60.0"
+ "playwright-core": "1.61.0"
},
"bin": {
"playwright": "cli.js"
@@ -10438,9 +10438,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
- "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz",
+ "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -12656,9 +12656,9 @@
}
},
"node_modules/tiptap-extension-code-block-prism": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/tiptap-extension-code-block-prism/-/tiptap-extension-code-block-prism-0.0.2.tgz",
- "integrity": "sha512-Rt5Q5G4qLMTWJ33jD9Otrkw9u0pu0pZYL3YIGe4AwQovxZe7RgKPaV5bopsX3JSTDuxDzyUjGGmEgR6US2A5Vg==",
+ "version": "0.0.2-e",
+ "resolved": "https://registry.npmjs.org/tiptap-extension-code-block-prism/-/tiptap-extension-code-block-prism-0.0.2-e.tgz",
+ "integrity": "sha512-zMeOJ+qk3r7qqpC0s1i4IM1Eax1WqQmNe9oYzLUHyWXmtaXAcc8FYOizdqJJdZallfoCeT2TuFQfR+Ulxzok5A==",
"license": "MIT",
"dependencies": {
"hast-util-from-html": "1.0.0",
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/package.json b/bases/rsptx/assignment_server_api/assignment_builder/package.json
index 1c822aa06..50e7ed42f 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/package.json
+++ b/bases/rsptx/assignment_server_api/assignment_builder/package.json
@@ -68,7 +68,7 @@
"reselect": "^5.1.1",
"screenfull": "^6.0.2",
"tippy.js": "^6.3.7",
- "tiptap-extension-code-block-prism": "^0.0.2-e",
+ "tiptap-extension-code-block-prism": "0.0.2-e",
"typesafe-actions": "^5.1.0",
"web-vitals": "^2.1.4"
},
@@ -105,7 +105,7 @@
},
"devDependencies": {
"@axe-core/playwright": "^4.11.3",
- "@playwright/test": "^1.60.0",
+ "@playwright/test": "1.61.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx
index 933a12d0d..be6d8b68c 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/ExercisePreview/ExercisePreviewModal.tsx
@@ -1,5 +1,5 @@
import { Modal } from "@mantine/core";
-import { useState, cloneElement, MouseEvent, ReactElement } from "react";
+import { cloneElement, MouseEvent, ReactElement, useState } from "react";
import { Exercise } from "@/types/exercises";
@@ -34,6 +34,7 @@ export const ExercisePreviewModal = ({
size="auto"
centered
title={questionName ? {questionName} : undefined}
+ closeButtonProps={{ "aria-label": "Close exercise preview" }}
>
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx
index 02bb80bfd..cd2452a33 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.spec.tsx
@@ -15,8 +15,8 @@ const {
} = vi.hoisted(() => ({
hookReturn: {
exercises: [
- { id: 1, name: "Ex One", question_type: "mchoice", qnumber: "Q1" },
- { id: 2, name: "Ex Two", question_type: "activecode", qnumber: "Q2" }
+ { id: 1, name: "Ex One", author: "Author One", question_type: "mchoice", qnumber: "Q1" },
+ { id: 2, name: "Ex Two", author: "Author Two", question_type: "activecode", qnumber: "Q2" }
],
pagination: { total: 2, page: 0, limit: 20, pages: 1 },
loading: false,
@@ -157,6 +157,7 @@ describe("SmartSearchExercises", () => {
renderWithMantine(
);
expect(screen.getByRole("heading", { name: "Search exercises" })).toBeInTheDocument();
expect(screen.getByText("Showing 2 of 2 exercises")).toBeInTheDocument();
+ expect(screen.queryByText("2 of 2 results shown.")).not.toBeInTheDocument();
});
it("returns to the list view from the toolbar back button", async () => {
@@ -217,4 +218,11 @@ describe("SmartSearchExercises", () => {
expect(monoCells.length).toBe(2);
expect(monoCells[0].textContent).toBe("Ex One");
});
+
+ it("does not duplicate visible Name and Author cell text in title attributes", () => {
+ renderWithMantine(
);
+
+ expect(screen.getByText("Ex One")).not.toHaveAttribute("title");
+ expect(screen.getByText("Author One")).not.toHaveAttribute("title");
+ });
});
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx
index 39d8ee379..a36a7550e 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx
@@ -16,8 +16,7 @@ import {
Switch,
TagsInput,
Text,
- Tooltip,
- VisuallyHidden
+ Tooltip
} from "@mantine/core";
import { datasetSelectors } from "@store/dataset/dataset.logic";
import {
@@ -53,11 +52,7 @@ type FilterMetaValue = { value: string | string[] | null };
const MATCH_CONTAINS = "contains";
const MATCH_IN = "in";
-const renderEllipsis = (text?: string | null) => (
-
- {text}
-
-);
+const renderEllipsis = (text?: string | null) =>
{text}
;
export const SmartSearchExercises = ({
setCurrentEditExercise,
@@ -284,7 +279,7 @@ export const SmartSearchExercises = ({
filter: { variant: "text", placeholder: "Search by name" }
},
cell: ({ row }) => (
-
+
{row.original.name}
)
@@ -381,7 +376,7 @@ export const SmartSearchExercises = ({
>
Search exercises}
+ aria-label="Search exercises"
value={globalTerms}
onChange={(terms) => onGlobalFilterChange(terms.join(" "))}
placeholder="Search terms…"
@@ -474,6 +469,7 @@ export const SmartSearchExercises = ({
ariaLabel="Search exercises"
fillHeight
scrollSentinelRef={sentinelRef}
+ showFilterResultsStatus={false}
/>
)}
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx
index bfb763fe8..cc8af00e3 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/ui/DataGrid.tsx
@@ -85,6 +85,7 @@ export interface DataGridProps
{
loading?: boolean;
emptyMessage?: React.ReactNode;
filterResultsLabel?: string;
+ showFilterResultsStatus?: boolean;
filterResultsTotal?: number;
tableFrameClassName?: string;
ariaLabel?: string;
@@ -190,6 +191,7 @@ export function DataGrid({
initialSorting = [],
loading = false,
emptyMessage = "Nothing here yet",
+ showFilterResultsStatus = true,
filterResultsLabel = "results",
filterResultsTotal = data.length,
tableFrameClassName,
@@ -251,7 +253,8 @@ export function DataGrid({
const resolvedPageCount = table.getPageCount();
const rows = table.getRowModel().rows;
const filteredRowCount = table.getFilteredRowModel().rows.length;
- const announcesFilterResults = enableGlobalFilter || enableColumnFilters;
+ const announcesFilterResults =
+ showFilterResultsStatus && (enableGlobalFilter || enableColumnFilters);
const tabbableRowId = rows.some((row) => row.id === keyboardRowId) ? keyboardRowId : rows[0]?.id;
// Column filters shrink the table from the inside, so a page index that came
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.spec.ts
index 9d02c9f4a..4da4d1a55 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.spec.ts
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.spec.ts
@@ -50,6 +50,13 @@ describe("course-aware browser history", () => {
}
);
+ it("does not redirect the Vite development shell without course metadata", () => {
+ expect(reconcileServerCourse("", "course-2", "/builder/22")).toBeNull();
+ expect(syncCourseHistoryEntry("", "/builder/22")).toBeNull();
+ expect(getRestoredCourseDestination(true, "", "/builder/22")).toBeNull();
+ expect(sessionStorage.getItem("assignment-builder.active-course")).toBeNull();
+ });
+
it("redirects a stale grader entry to the grader assignment list", () => {
syncCourseHistoryEntry("course-1", "/grader/22");
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.ts
index f8d18ed35..5970c238f 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.ts
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/courseHistory.ts
@@ -82,7 +82,11 @@ export const reconcileServerCourse = (
serverCourse: string | undefined,
pathname: string
): string | null => {
- if (!serverCourse || serverCourse === pageCourse) return null;
+ // The Vite development shell intentionally has no course metadata. In that
+ // environment the authenticated API can still report a current course, but
+ // treating it as a mismatch creates a redirect loop between the production
+ // route and the Vite client route.
+ if (!pageCourse || !serverCourse || serverCourse === pageCourse) return null;
markCourseSwitch(serverCourse);
return getCourseSwitchDestination(pathname);
@@ -93,6 +97,8 @@ export const reconcileServerCourse = (
* from another course is stale and must not restore an assignment-specific URL.
*/
export const syncCourseHistoryEntry = (currentCourse: string, pathname: string): string | null => {
+ if (!currentCourse) return null;
+
const entryCourse = getHistoryState()[HISTORY_COURSE_KEY];
const activeCourse = getStoredCourse();
@@ -137,7 +143,7 @@ export const getRestoredCourseDestination = (
pageCourse: string,
pathname: string
): string | null => {
- if (!persisted) return null;
+ if (!persisted || !pageCourse) return null;
const activeCourse = getStoredCourse();
const activeCourseMismatch = Boolean(activeCourse && activeCourse !== pageCourse);