From b1e6794484a0f8d32063fba80d8ea633ba914381 Mon Sep 17 00:00:00 2001 From: "gustavo.bernardes" Date: Mon, 4 Mar 2024 13:00:47 -0300 Subject: [PATCH] implement react router for all routes in sidenav SideNav is now working for all routes Instead ERB Html now is using React.Router closes FOO-3886 closes FOO-3982 closes FOO-4326 closes FOO-4327 flag=instui_nav test plan: - Log in to Canvas as an Admin - Go to RootAccount > Settings > Feature Options > Enable New InstUI NavBar - This should render the new SideNav - Navbar should be displayed Change-Id: Ic6d88dfb5284358846ee192cad6cbce7c8300da9 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/342010 QA-Review: Gustavo Bernardes Product-Review: Gustavo Bernardes Tested-by: Service Cloud Jenkins Reviewed-by: Gustavo Bernardes Reviewed-by: Charley Kline --- app/stylesheets/base/_ic_app_header.scss | 16 ++- .../navigation/new_navigation_spec.rb | 109 ++++++++++++++++++ ui/boot/initializers/router.tsx | 2 +- ui/features/navigation_header/index.tsx | 3 +- .../react/MobileGlobalMenu.tsx | 2 +- .../react/NavigationHeaderRoute.tsx | 6 +- .../navigation_header/react/SideNav.tsx | 89 ++++++++++++-- ui/features/navigation_header/react/utils.ts | 4 +- 8 files changed, 210 insertions(+), 21 deletions(-) create mode 100644 spec/selenium/navigation/new_navigation_spec.rb diff --git a/app/stylesheets/base/_ic_app_header.scss b/app/stylesheets/base/_ic_app_header.scss index cf740c5afa1..75ac197df60 100644 --- a/app/stylesheets/base/_ic_app_header.scss +++ b/app/stylesheets/base/_ic_app_header.scss @@ -102,6 +102,12 @@ $ic-tooltip-arrow-size: 0.375rem; } } + .ic-svg-external-tool>div:first-child { + svg>path { + transform: scale(0.4); + } + } + .ic-sidenav-tray:active:hover, .ic-user-tray:active:hover { color: var(--ic-brand-primary); @@ -127,7 +133,6 @@ $ic-tooltip-arrow-size: 0.375rem; .ic-user-avatar { width: 1.875rem; height: 1.875rem; - border: 2px solid var(--ic-brand-global-nav-avatar-border) !important; } .ic-collapse-div { @@ -438,8 +443,6 @@ body.primary-nav-expanded { gap: 0.2rem !important; width: auto !important; height: 63.55px !important; - font-weight: 400; - transition: background-color 0.3s; } .ic-user-tray { @@ -463,6 +466,12 @@ body.primary-nav-expanded { } } + .ic-svg-external-tool>div:first-child { + svg>path { + transform: scale(0.4); + } + } + .ic-sidenav-tray:active:hover, .ic-user-tray:active:hover { color: var(--ic-brand-primary); @@ -488,7 +497,6 @@ body.primary-nav-expanded { .ic-user-avatar { width: 2.25rem; height: 2.25rem; - border: 2px solid var(--ic-brand-global-nav-avatar-border) !important; } /** New SideNav CSS */ diff --git a/spec/selenium/navigation/new_navigation_spec.rb b/spec/selenium/navigation/new_navigation_spec.rb new file mode 100644 index 00000000000..f3c3f6af3fc --- /dev/null +++ b/spec/selenium/navigation/new_navigation_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +# +# Copyright (C) 2024 - present Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . + +require_relative "../common" +require_relative "../../helpers/k5_common" + +describe "New SideNav Navigation" do + include_context "in-process server selenium tests" + include K5Common + + context "As a Teacher" do + before do + course_with_teacher_logged_in + @course.root_account.enable_feature!(:instui_nav) + end + + it "minimizes and expand the side nav when clicked" do + get "/" + primary_nav_toggle = f("#sidenav-toggle") + primary_nav_toggle.click + wait_for_ajaximations + expect(f("body")).not_to have_class("primary-nav-expanded") + primary_nav_toggle.click + wait_for_ajaximations + expect(f("body")).to have_class("primary-nav-expanded") + end + + describe "Profile Link" do + it "shows the profile tray upon clicking" do + get "/" + user_tray = f("#user-tray") + user_tray.click + wait_for_ajaximations + expect(f('[aria-label="User profile picture"]')).to be_displayed + end + end + + describe "Courses Link" do + it "shows the courses tray upon clicking" do + get "/" + courses_tray = f("#courses-tray") + courses_tray.click + wait_for_ajaximations + expect(f("[aria-label='Courses tray']")).to be_displayed + end + end + + describe "LTI Tools" do + it "shows a custom logo/link for LTI tools" do + @tool = Account.default.context_external_tools.new({ + name: "Commons", + domain: "canvaslms.com", + consumer_key: "12345", + shared_secret: "secret" + }) + @tool.set_extension_setting(:global_navigation, { + url: "canvaslms.com", + visibility: "admins", + display_type: "full_width", + text: "Commons", + icon_svg_path_64: "M100,37L70.1,10.5v17.6H38.6c-4.9,0-8.8,3.9-8.8,8.8s3.9,8.8,8.8,8.8h31.5v17.6L100,37z" + }) + @tool.save! + get "/" + expect(f("#external-tool-tray")).to be_displayed + end + end + + describe "Recent History" do + before do + Setting.set("enable_page_views", "db") + @assignment = @course.assignments.create(name: "another assessment") + @quiz = Quizzes::Quiz.create!(title: "quiz1", context: @course) + page_view_for url: app_url + "/courses/#{@course.id}/assignments/#{@assignment.id}", + context: @course, + created_at: 5.minutes.ago, + asset_category: "assignments", + asset_code: @assignment.asset_string + page_view_for url: app_url + "/courses/#{@course.id}/quizzes/#{@quiz.id}", + context: @course, + created_at: 1.minute.ago, + asset_category: "quizzes", + asset_code: @quiz.asset_string + end + + it "shows the Recent History tray upon clicking" do + get "/" + wait_for_ajaximations + expect(f("#history-tray")).to be_displayed + end + end + end +end diff --git a/ui/boot/initializers/router.tsx b/ui/boot/initializers/router.tsx index cea726faa9d..b522de0de74 100644 --- a/ui/boot/initializers/router.tsx +++ b/ui/boot/initializers/router.tsx @@ -49,7 +49,7 @@ const portalRouter = createBrowserRouter( {accountGradingSettingsRoutes} {(window.ENV.FEATURES.instui_nav || localStorage.instui_nav_dev) && - ['/', '/accounts/*', '/calendar/*', '/courses/*', '/conversations/*'].map(path => ( + ['/', '/*', '/*/*'].map(path => ( { const mobileContextNavContainer = document.getElementById('mobileContextNavContainer') ReactDOM.render( - + , mobileContextNavContainer, () => { diff --git a/ui/features/navigation_header/react/MobileGlobalMenu.tsx b/ui/features/navigation_header/react/MobileGlobalMenu.tsx index 09c4261c1f8..92ab439c620 100644 --- a/ui/features/navigation_header/react/MobileGlobalMenu.tsx +++ b/ui/features/navigation_header/react/MobileGlobalMenu.tsx @@ -264,7 +264,7 @@ export default function MobileGlobalMenu(props: Props) { xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" - dangerouslySetInnerHTML={{__html: tool.svgPath}} + dangerouslySetInnerHTML={{__html: tool.svgPath ?? ''}} width="1em" height="1em" aria-hidden="true" diff --git a/ui/features/navigation_header/react/NavigationHeaderRoute.tsx b/ui/features/navigation_header/react/NavigationHeaderRoute.tsx index 25370f2982c..1671c295b2c 100644 --- a/ui/features/navigation_header/react/NavigationHeaderRoute.tsx +++ b/ui/features/navigation_header/react/NavigationHeaderRoute.tsx @@ -16,11 +16,13 @@ * with this program. If not, see . */ -import React from 'react' +import React, {useMemo} from 'react' import {Portal} from '@instructure/ui-portal' import SideNav from './SideNav' +import {getExternalTools, type ExternalTool} from './utils' export function Component() { + const externalTools = useMemo(() => getExternalTools(), []) const mountPoint: HTMLElement | null = document.getElementById('header') if (!mountPoint) { return null @@ -28,7 +30,7 @@ export function Component() { mountPoint.innerHTML = '' return ( - + ) } diff --git a/ui/features/navigation_header/react/SideNav.tsx b/ui/features/navigation_header/react/SideNav.tsx index 353037f4ab9..d5a9e6fd739 100644 --- a/ui/features/navigation_header/react/SideNav.tsx +++ b/ui/features/navigation_header/react/SideNav.tsx @@ -29,6 +29,7 @@ import { IconAdminLine, IconCalendarMonthLine, IconCanvasLogoSolid, + IconClockLine, IconCoursesLine, IconDashboardLine, IconFolderLine, @@ -46,7 +47,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {getUnreadCount} from './queries/unreadCountQuery' import {getSetting, setSetting} from './queries/settingsQuery' import {getActiveItem, getTrayLabel, getTrayPortal} from './utils' -import type {ActiveTray} from './utils' +import type {ActiveTray, ExternalTool} from './utils' const I18n = useI18nScope('sidenav') @@ -66,25 +67,27 @@ export const InformationIconEnum = { const defaultActiveItem = getActiveItem() -const SideNav = () => { +const SideNav = ({externalTools = []}: {externalTools?: ExternalTool[]}) => { const [isTrayOpen, setIsTrayOpen] = useState(false) const [activeTray, setActiveTray] = useState(null) const [selectedNavItem, setSelectedNavItem] = useState(defaultActiveItem) const sideNavRef = useRef(null) + const logoRef = useRef(null) const accountRef = useRef(null) + const adminRef = useRef(null) const dashboardRef = useRef(null) const coursesRef = useRef(null) - const adminRef = useRef(null) const calendarRef = useRef(null) const inboxRef = useRef(null) + const historyRef = useRef(null) + const externalTool = useRef(null) const helpRef = useRef(null) - const logoRef = useRef(null) // after tray is closed, eventually set activeTray to null // we don't do this immediately in order to maintain animation of closing tray useEffect(() => { if (!isTrayOpen) { - setTimeout(() => setActiveTray(null), 150) + setTimeout(() => setActiveTray(null), 100) } }, [isTrayOpen]) @@ -113,13 +116,11 @@ const SideNav = () => { accountRef.current.dataset.selected = 'true' } break - case 'accounts': if (adminRef.current instanceof HTMLElement) { adminRef.current.dataset.selected = 'true' } break - case 'dashboard': if (dashboardRef.current instanceof HTMLElement) { dashboardRef.current.dataset.selected = 'true' @@ -157,6 +158,8 @@ const SideNav = () => { contentPadding: '0.1rem', backgroundColor: 'transparent', hoverBackgroundColor: 'transparent', + fontWeight: 400, + linkTextDecoration: 'inherit', } const getHelpIcon = (): JSX.Element => { @@ -176,11 +179,11 @@ const SideNav = () => { const countsEnabled = Boolean( window.ENV.current_user_id && !window.ENV.current_user?.fake_student ) + const brandConfig = (window.ENV.active_brand_config as { variables: {'ic-brand-header-image': string} }) ?? null - if (brandConfig) { const variables = brandConfig.variables logoUrl = variables['ic-brand-header-image'] @@ -246,11 +249,20 @@ const SideNav = () => { document.querySelector('#courses-tray'), document.querySelector('#calendar-tray'), document.querySelector('#inbox-tray'), + document.querySelector('#history-tray'), + document.querySelector('#external-tool-tray'), document.querySelector('#help-tray'), ] if (Array.isArray(sideNavTrays)) sideNavTrays.forEach(sideNavTray => sideNavTray?.classList.add('ic-sidenav-tray')) + const externalToolsSvgImg = ['ic-svg-external-tool', 'ic-img-external-tool'] + + if (Array.isArray(externalToolsSvgImg)) + externalToolsSvgImg.forEach(svgImgClassName => + document.querySelector('#external-tool-tray')?.classList.add(svgImgClassName) + ) + document.querySelector('#user-tray')?.classList.add('ic-user-tray') document.querySelector('#canvas-logo')?.classList.add('ic-canvas-logo') document.querySelector('#brand-logo')?.classList.add('ic-brand-logo') @@ -261,6 +273,7 @@ const SideNav = () => { const collapseButton = collapseDiv.childNodes[0] as HTMLElement collapseDiv.classList.add('ic-collapse-div') collapseButton.classList.add('ic-collapse-button') + collapseButton.id = 'sidenav-toggle' if (collapseGlobalNav) document.body.classList.remove('primary-nav-expanded') else document.body.classList.add('primary-nav-expanded') @@ -324,6 +337,7 @@ const SideNav = () => { ...navItemThemeOverride, contentPadding: '0', }} + minimized={collapseGlobalNav} data-testid="sidenav-header-logo" /> { src={window.ENV.current_user.avatar_image_url} data-testid="sidenav-user-avatar" showBorder="always" - frameBorder={2} + frameBorder={4} themeOverride={{ background: 'transparent', + borderColor: '#ffffff', + borderWidthSmall: '0.2em', + borderWidthMedium: '0.2rem', }} /> @@ -372,6 +389,7 @@ const SideNav = () => { }} selected={selectedNavItem === 'profile'} themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} /> { }} selected={selectedNavItem === 'accounts'} themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} /> { icon={isK5User ? : } label={isK5User ? I18n.t('Home') : I18n.t('Dashboard')} href="/" - themeOverride={navItemThemeOverride} selected={selectedNavItem === 'dashboard'} + themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} /> { }} selected={selectedNavItem === 'courses'} themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} /> { icon={} label={I18n.t('Calendar')} href="/calendar" - themeOverride={navItemThemeOverride} selected={selectedNavItem === 'calendar'} + themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} /> { href="/conversations" selected={selectedNavItem === 'conversations'} themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} /> + (historyRef.current = el)} + icon={} + label={I18n.t('History')} + href={window.ENV.page_view_update_url} + selected={selectedNavItem === 'history'} + themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} + /> + {externalTools && + externalTools.map(tool => ( + (externalTool.current = el)} + icon={ + 'svgPath' in tool ? ( + + ) : ( + + ) + } + label={tool.label} + href={tool.href?.toString()} + selected={tool.isActive} + themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} + /> + ))} { }} selected={selectedNavItem === 'help'} themeOverride={navItemThemeOverride} + minimized={collapseGlobalNav} />