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..1fb62c068 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.64.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", @@ -3394,19 +3394,19 @@ } }, "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.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.60.0" + "playwright": "1.64.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@popperjs/core": { @@ -10419,50 +10419,32 @@ } }, "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.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.60.0" + "playwright-core": "1.64.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "node": ">=20" } }, "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.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=20" } }, "node_modules/possible-typed-array-names": { @@ -12656,9 +12638,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..67b8ff876 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.64.1", "@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/shell/AppNavBar.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/shell/AppNavBar.spec.tsx index 5c423fa46..051564993 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/shell/AppNavBar.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/shell/AppNavBar.spec.tsx @@ -1,59 +1,94 @@ import userEvent from "@testing-library/user-event"; import { NavItem } from "@/navUtils"; -import { renderWithMantine, screen, waitFor, within } from "@/test/renderWithMantine"; +import { fireEvent, renderWithMantine, screen, waitFor, within } from "@/test/renderWithMantine"; import { AppNavBar } from "./AppNavBar"; const makeItems = (overrides: Partial[] = []): NavItem[] => [ - { label: "Home", icon: "home", command: vi.fn() }, - { label: "Back to Course", command: vi.fn() }, + { label: "Home", icon: "home", onClick: vi.fn() }, + { label: "Back to Course", onClick: vi.fn() }, { label: "User", icon: "user", items: [ { label: "Welcome alice", icon: "bolt" }, { separator: true }, - { label: "Logout", command: vi.fn() }, - { label: "Hidden", command: vi.fn(), visible: false } + { label: "Logout", onClick: vi.fn() }, + { label: "Hidden", onClick: vi.fn(), visible: false } ] }, ...(overrides as NavItem[]) ]; describe("AppNavBar", () => { - it("renders top-level item labels as buttons", () => { - renderWithMantine(); + it("renders top-level navigation destinations as links", () => { + const items: NavItem[] = [ + { label: "Grader", href: "/assignment/instructor/grader", onClick: vi.fn() }, + { label: "Back to Course", href: "/ns/course/index", onClick: vi.fn() }, + { label: "User", items: [{ label: "Logout", onClick: vi.fn() }] } + ]; + + renderWithMantine(); - const home = screen.getByRole("button", { name: /home/i }); - const backToCourse = screen.getByRole("button", { name: /back to course/i }); + const grader = screen.getByRole("link", { name: /grader/i }); + const backToCourse = screen.getByRole("link", { name: /back to course/i }); const user = screen.getByRole("button", { name: /user/i }); - expect(home).toBeInTheDocument(); + expect(grader).toHaveAttribute("href", "/assignment/instructor/grader"); expect(backToCourse).toBeInTheDocument(); expect(user).toBeInTheDocument(); - expect(home).not.toHaveAttribute("title"); + expect(grader).not.toHaveAttribute("title"); expect(backToCourse).not.toHaveAttribute("title"); expect(user).not.toHaveAttribute("title"); }); - it("invokes the command when a top-level item is clicked", async () => { - const command = vi.fn(); - const items: NavItem[] = [{ label: "Grader", icon: "check-square", command }]; + it("invokes onClick when a top-level item is clicked", async () => { + const onClick = vi.fn(); + const items: NavItem[] = [ + { + label: "Grader", + icon: "check-square", + href: "/assignment/instructor/grader", + onClick + } + ]; renderWithMantine(); - await userEvent.click(screen.getByRole("button", { name: /grader/i })); + await userEvent.click(screen.getByRole("link", { name: /grader/i })); - expect(command).toHaveBeenCalledTimes(1); + expect(onClick).toHaveBeenCalledTimes(1); }); - it("opens a submenu and invokes a sub-item command", async () => { + it.each(["ctrlKey", "metaKey", "shiftKey"] as const)( + "leaves modified clicks to the browser when %s is held", + (modifier) => { + const onClick = vi.fn(); + const items: NavItem[] = [ + { + label: "Grader", + href: "/assignment/instructor/grader", + onClick + } + ]; + + renderWithMantine(); + const link = screen.getByRole("link", { name: /grader/i }); + + link.addEventListener("click", (event) => event.preventDefault()); + fireEvent.click(link, { [modifier]: true }); + + expect(onClick).not.toHaveBeenCalled(); + } + ); + + it("opens a submenu and invokes the sub-item onClick callback", async () => { const logout = vi.fn(); const items: NavItem[] = [ { label: "User", icon: "user", - items: [{ label: "Logout", command: logout }] + items: [{ label: "Logout", onClick: logout }] } ]; @@ -67,13 +102,39 @@ describe("AppNavBar", () => { await waitFor(() => expect(logout).toHaveBeenCalledTimes(1)); }); + it("renders submenu destinations as links and preserves modified clicks", async () => { + const onClick = vi.fn(); + const items: NavItem[] = [ + { + label: "Help", + items: [ + { + label: "Guide", + href: "https://guide.runestone.academy/", + onClick + } + ] + } + ]; + + renderWithMantine(); + await userEvent.click(screen.getByRole("button", { name: /help/i })); + const guide = await screen.findByRole("menuitem", { name: /guide/i }); + + expect(guide).toHaveAttribute("href", "https://guide.runestone.academy/"); + guide.addEventListener("click", (event) => event.preventDefault()); + fireEvent.click(guide, { ctrlKey: true }); + + expect(onClick).not.toHaveBeenCalled(); + }); + it("supports arrow navigation and closes a submenu with Escape", async () => { const items: NavItem[] = [ { label: "Help", items: [ - { label: "Guide", command: vi.fn() }, - { label: "Contact", command: vi.fn() } + { label: "Guide", onClick: vi.fn() }, + { label: "Contact", onClick: vi.fn() } ] } ]; @@ -107,8 +168,8 @@ describe("AppNavBar", () => { it("hides top-level items whose visible flag is false", () => { const items: NavItem[] = [ - { label: "Shown", command: vi.fn() }, - { label: "Gone", command: vi.fn(), visible: false } + { label: "Shown", onClick: vi.fn() }, + { label: "Gone", onClick: vi.fn(), visible: false } ]; renderWithMantine(); @@ -118,11 +179,11 @@ describe("AppNavBar", () => { }); it("does not invoke disabled course items", async () => { - const command = vi.fn(); + const onClick = vi.fn(); const items: NavItem[] = [ { label: "Course: cs101", - items: [{ label: "cs102", command, disabled: true }] + items: [{ label: "cs102", onClick, disabled: true }] } ]; @@ -130,7 +191,7 @@ describe("AppNavBar", () => { await userEvent.click(screen.getByRole("button", { name: /course: cs101/i })); await userEvent.click(await screen.findByRole("menuitem", { name: /cs102/i })); - expect(command).not.toHaveBeenCalled(); + expect(onClick).not.toHaveBeenCalled(); }); it("renders the Runestone logo image alongside the wordmark", () => { @@ -146,8 +207,8 @@ describe("AppNavBar", () => { it("marks the item matching the active path with aria-current", () => { const items: NavItem[] = [ - { label: "Grader", command: vi.fn(), activePrefixes: ["/grader"] }, - { label: "Builder", command: vi.fn(), activePrefixes: ["/builder", "/"] } + { label: "Grader", onClick: vi.fn(), activePrefixes: ["/grader"] }, + { label: "Builder", onClick: vi.fn(), activePrefixes: ["/builder", "/"] } ]; renderWithMantine(); @@ -157,7 +218,7 @@ describe("AppNavBar", () => { }); it("marks no item active when the path matches nothing", () => { - const items: NavItem[] = [{ label: "Grader", command: vi.fn(), activePrefixes: ["/grader"] }]; + const items: NavItem[] = [{ label: "Grader", onClick: vi.fn(), activePrefixes: ["/grader"] }]; renderWithMantine(); @@ -179,13 +240,13 @@ describe("AppNavBar", () => { const home = vi.fn(); const logout = vi.fn(); const items: NavItem[] = [ - { label: "Home", icon: "home", command: home }, + { label: "Home", icon: "home", onClick: home }, { label: "User", icon: "user", items: [ - { label: "Logout", command: logout }, - { label: "Hidden", command: vi.fn(), visible: false } + { label: "Logout", onClick: logout }, + { label: "Hidden", onClick: vi.fn(), visible: false } ] } ]; @@ -202,4 +263,25 @@ describe("AppNavBar", () => { await userEvent.click(within(menu).getByRole("menuitem", { name: /home/i })); await waitFor(() => expect(home).toHaveBeenCalledTimes(1)); }); + + it("renders compact navigation destinations as links", async () => { + const onClick = vi.fn(); + const items: NavItem[] = [ + { + label: "Grader", + href: "/assignment/instructor/grader", + onClick + } + ]; + + renderWithMantine(); + await userEvent.click(screen.getByRole("button", { name: /open navigation menu/i })); + const grader = await screen.findByRole("menuitem", { name: /grader/i }); + + expect(grader).toHaveAttribute("href", "/assignment/instructor/grader"); + grader.addEventListener("click", (event) => event.preventDefault()); + fireEvent.click(grader, { metaKey: true }); + + expect(onClick).not.toHaveBeenCalled(); + }); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/shell/AppNavBar.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/shell/AppNavBar.tsx index 185a864a9..c6a1b9b1d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/shell/AppNavBar.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/shell/AppNavBar.tsx @@ -1,6 +1,6 @@ import { Icon } from "@components/ui/Icon"; import { Button, Menu } from "@mantine/core"; -import { Fragment } from "react"; +import { Fragment, MouseEvent } from "react"; import { isNavItemActive, NavItem } from "@/navUtils"; @@ -14,6 +14,17 @@ interface AppNavBarProps { const menuTransitionProps = { transition: "pop", duration: 180 } as const; +const handleNavClick = (item: NavItem) => (event: MouseEvent) => { + if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || event.button !== 0) { + return; + } + + if (item.onClick) { + event.preventDefault(); + item.onClick(); + } +}; + const renderSubItem = (item: NavItem, index: number) => { if (item.separator) { return ; @@ -24,9 +35,11 @@ const renderSubItem = (item: NavItem, index: number) => { return ( {item.label} @@ -68,6 +81,8 @@ const renderTopItem = (item: NavItem, index: number, activePath: string) => { return ( @@ -100,10 +115,12 @@ const renderCompactItem = (item: NavItem, index: number, activePath: string) => return ( {item.label} diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts index 8a158ec28..6c5fc5dbf 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.spec.ts @@ -147,20 +147,20 @@ describe("buildNavBar", () => { it("opens http URLs in a new tab", () => { const items = buildNavBar(makeConfig()); const helpMenu = items.find((i) => i.label === "Help"); - const subItems = (helpMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; + const subItems = (helpMenu?.items as Array<{ label?: string; onClick?: () => void }>) ?? []; const guide = subItems.find((s) => s.label === "Instructor's guide"); - guide?.command?.(); + guide?.onClick?.(); expect(openSpy).toHaveBeenCalledWith("https://guide.runestone.academy/", "_blank"); }); it("opens https URLs in a new tab", () => { const items = buildNavBar(makeConfig()); const helpMenu = items.find((i) => i.label === "Help"); - const subItems = (helpMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; + const subItems = (helpMenu?.items as Array<{ label?: string; onClick?: () => void }>) ?? []; const discord = subItems.find((s) => s.label === "Join our Discord"); - discord?.command?.(); + discord?.onClick?.(); expect(openSpy).toHaveBeenCalledWith("https://discord.gg/f3Qmbk9P3U", "_blank"); }); }); @@ -170,17 +170,17 @@ describe("buildNavBar", () => { const items = buildNavBar(makeConfig({ course: "cs101" })); const back = items.find((i) => i.label?.startsWith("Back to")); - back?.command?.(); + back?.onClick?.(); expect(hrefSpy).toHaveBeenCalledWith("/ns/books/published/cs101/index.html"); }); it("sets window.location.href for /assignment paths", () => { const items = buildNavBar(makeConfig()); const userMenu = items.find((i) => i.label === "User"); - const subItems = (userMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; + const subItems = (userMenu?.items as Array<{ label?: string; onClick?: () => void }>) ?? []; const assignments = subItems.find((s) => s.label === "Assignments"); - assignments?.command?.(); + assignments?.onClick?.(); expect(hrefSpy).toHaveBeenCalledWith("/assignment/student/chooseAssignment"); }); @@ -188,7 +188,7 @@ describe("buildNavBar", () => { const items = buildNavBar(makeConfig()); const dashboard = items.find((i) => i.label === "Dashboard"); - dashboard?.command?.(); + dashboard?.onClick?.(); expect(hrefSpy).toHaveBeenCalledWith("/admin/instructor/menu"); }); }); @@ -198,7 +198,7 @@ describe("buildNavBar", () => { const items = buildNavBar(makeConfig()); const grader = items.find((i) => i.label === "Grader"); - grader?.command?.(); + grader?.onClick?.(); expect(hashSpy).toHaveBeenCalledWith("#/grader"); }); @@ -206,7 +206,7 @@ describe("buildNavBar", () => { const items = buildNavBar(makeConfig()); const builder = items.find((i) => i.label === "Assignment Builder"); - builder?.command?.(); + builder?.onClick?.(); expect(hashSpy).toHaveBeenCalledWith("#/builder"); }); }); @@ -217,7 +217,7 @@ describe("buildNavBar", () => { const items = buildNavBar(makeConfig(), navigate); const grader = items.find((i) => i.label === "Grader"); - grader?.command?.(); + grader?.onClick?.(); expect(navigate).toHaveBeenCalledWith("/grader"); }); @@ -226,7 +226,7 @@ describe("buildNavBar", () => { const items = buildNavBar(makeConfig(), navigate); const builder = items.find((i) => i.label === "Assignment Builder"); - builder?.command?.(); + builder?.onClick?.(); expect(navigate).toHaveBeenCalledWith("/builder"); }); @@ -235,7 +235,7 @@ describe("buildNavBar", () => { const items = buildNavBar(makeConfig({ course: "cs101" }), navigate); const back = items.find((i) => i.label?.startsWith("Back to")); - back?.command?.(); + back?.onClick?.(); expect(hrefSpy).toHaveBeenCalledWith("/ns/books/published/cs101/index.html"); expect(navigate).not.toHaveBeenCalled(); }); @@ -244,10 +244,10 @@ describe("buildNavBar", () => { const navigate = vi.fn(); const items = buildNavBar(makeConfig(), navigate); const helpMenu = items.find((i) => i.label === "Help"); - const subItems = (helpMenu?.items as Array<{ label?: string; command?: () => void }>) ?? []; + const subItems = (helpMenu?.items as Array<{ label?: string; onClick?: () => void }>) ?? []; const guide = subItems.find((s) => s.label === "Instructor's guide"); - guide?.command?.(); + guide?.onClick?.(); expect(openSpy).toHaveBeenCalledWith("https://guide.runestone.academy/", "_blank"); expect(navigate).not.toHaveBeenCalled(); }); @@ -264,6 +264,24 @@ describe("buildNavBar", () => { }); }); + it("provides real destinations for navigation links", () => { + const items = buildNavBar(makeConfig()); + const userItems = items.find((item) => item.label === "User")?.items; + const helpItems = items.find((item) => item.label === "Help")?.items; + + expect(items.find((item) => item.label === "Grader")?.href).toBe( + "/assignment/instructor/grader" + ); + expect(items.find((item) => item.label === "Assignment Builder")?.href).toBe( + "/assignment/instructor/builder" + ); + expect(userItems?.find((item) => item.label === "Course home")?.href).toBe("/ns/course/index"); + expect(helpItems?.find((item) => item.label === "Instructor's guide")?.href).toBe( + "https://guide.runestone.academy/" + ); + expect(userItems?.find((item) => item.label === "Logout")?.href).toBeUndefined(); + }); + describe("course switcher", () => { it("replaces Home with the current course and recent instructor courses", () => { const onSwitch = vi.fn(); @@ -280,7 +298,7 @@ describe("buildNavBar", () => { expect(courseMenu?.items?.map((item) => item.label)).toEqual(["cs101", "cs102"]); expect(courseMenu?.items?.[0]).toMatchObject({ icon: "check", disabled: true }); - courseMenu?.items?.[1].command?.(); + courseMenu?.items?.[1].onClick?.(); expect(onSwitch).toHaveBeenCalledWith("cs102"); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.ts index 71d2f041d..1ec4ae258 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/navUtils.ts @@ -5,7 +5,8 @@ import { isEmptyObject } from "@/utils/object"; export interface NavItem { label?: string; icon?: PrimeIconName; - command?: () => void; + href?: string; + onClick?: () => void; items?: NavItem[]; visible?: boolean; separator?: boolean; @@ -79,7 +80,7 @@ export const buildNavBar = ( ? courseSwitcher.courses.map((availableCourse) => ({ label: availableCourse.course_name, icon: availableCourse.is_current ? "check" : undefined, - command: availableCourse.is_current + onClick: availableCourse.is_current ? undefined : () => courseSwitcher.onSwitch(availableCourse.course_name), disabled: availableCourse.is_current || courseSwitcher.isSwitching @@ -98,23 +99,27 @@ export const buildNavBar = ( }, { label: `Back to ${course || "Course"}`, - command: () => navigateToPath("/ns/books/published/" + course + "/index.html") + href: `/ns/books/published/${course}/index.html`, + onClick: () => navigateToPath("/ns/books/published/" + course + "/index.html") }, { label: "Dashboard", icon: "cog", - command: () => navigateToPath("/admin/instructor/menu") + href: "/admin/instructor/menu", + onClick: () => navigateToPath("/admin/instructor/menu") }, { label: "Grader", icon: "check-square", - command: () => navigateToPath("grader"), + href: "/assignment/instructor/grader", + onClick: () => navigateToPath("grader"), activePrefixes: ["/grader"] }, { label: "Assignment Builder", icon: "file-edit", - command: () => navigateToPath("builder"), + href: "/assignment/instructor/builder", + onClick: () => navigateToPath("builder"), activePrefixes: ["/builder", "/"] }, { @@ -128,64 +133,75 @@ export const buildNavBar = ( { label: "Course home", icon: "home", - command: () => navigateToPath("/ns/course/index") + href: "/ns/course/index", + onClick: () => navigateToPath("/ns/course/index") }, { label: "Assignments", icon: "pencil", - command: () => navigateToPath("/assignment/student/chooseAssignment") + href: "/assignment/student/chooseAssignment", + onClick: () => navigateToPath("/assignment/student/chooseAssignment") }, { label: "Practice", icon: "palette", - command: () => navigateToPath("/runestone/assignments/practice") + href: "/runestone/assignments/practice", + onClick: () => navigateToPath("/runestone/assignments/practice") }, { label: "Peer instruction (instructor)", - command: () => navigateToPath("/assignment/peer/instructor"), + href: "/assignment/peer/instructor", + onClick: () => navigateToPath("/assignment/peer/instructor"), visible: isInstructor }, { label: "Peer instruction (student)", - command: () => navigateToPath("/assignment/peer/student") + href: "/assignment/peer/student", + onClick: () => navigateToPath("/assignment/peer/student") }, { separator: true }, { label: "Change course", - command: () => navigateToPath("/admin/auth/my_courses") + href: "/admin/auth/my_courses", + onClick: () => navigateToPath("/admin/auth/my_courses") }, { separator: true }, { label: "Instructor's dashboard", - command: () => navigateToPath("/admin/instructor/menu") + href: "/admin/instructor/menu", + onClick: () => navigateToPath("/admin/instructor/menu") }, { label: "Accommodations", icon: "ban", - command: () => navigateToPath("except") + href: "/assignment/instructor/except", + onClick: () => navigateToPath("except") }, { label: "Progress page", - command: () => navigateToPath("/assignment/student/studentreport") + href: "/assignment/student/studentreport", + onClick: () => navigateToPath("/assignment/student/studentreport") }, { separator: true }, { label: "Edit profile", - command: () => navigateToPath("/admin/auth/profile") + href: "/admin/auth/profile", + onClick: () => navigateToPath("/admin/auth/profile") }, { label: "Change password", - command: () => navigateToPath("/runestone/default/user/change_password") + href: "/runestone/default/user/change_password", + onClick: () => navigateToPath("/runestone/default/user/change_password") }, { label: "Logout", - command: () => navigateToPath("/admin/auth/logout") + onClick: () => navigateToPath("/admin/auth/logout") } ] }, @@ -196,12 +212,14 @@ export const buildNavBar = ( { label: "Instructor's guide", icon: "book", - command: () => navigateToPath("https://guide.runestone.academy/") + href: "https://guide.runestone.academy/", + onClick: () => navigateToPath("https://guide.runestone.academy/") }, { label: "Video tutorials", icon: "video", - command: () => + href: "https://www.youtube.com/playlist?list=PLnjfglXW2QQRG6NRsg5mq8V99MM_3j_ZU", + onClick: () => navigateToPath( "https://www.youtube.com/playlist?list=PLnjfglXW2QQRG6NRsg5mq8V99MM_3j_ZU" ) @@ -209,12 +227,14 @@ export const buildNavBar = ( { label: "Contact us", icon: "envelope", - command: () => navigateToPath("https://github.com/RunestoneInteractive/rs/issues") + href: "https://github.com/RunestoneInteractive/rs/issues", + onClick: () => navigateToPath("https://github.com/RunestoneInteractive/rs/issues") }, { label: "Join our Discord", icon: "discord", - command: () => navigateToPath("https://discord.gg/f3Qmbk9P3U") + href: "https://discord.gg/f3Qmbk9P3U", + onClick: () => navigateToPath("https://discord.gg/f3Qmbk9P3U") } ] } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/vite.config.ts b/bases/rsptx/assignment_server_api/assignment_builder/vite.config.ts index bad4fcba3..d51afc4ec 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/vite.config.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/vite.config.ts @@ -56,7 +56,8 @@ export default defineConfig(({ mode }: { mode: string }) => { }, plugins: [react()], optimizeDeps: { - include: ["react", "react-dom"] + include: ["react", "react-dom", "tiptap-extension-code-block-prism"], + needsInterop: ["tiptap-extension-code-block-prism"] } }; });