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);