add TopNavigationTools
closes ADV-104 flag=top_navigation_placement Test plan: - NOTE: You must restart the web container after running Setting.set - PreReqs: - A tool with the `top_navigation` placement - On a Canvas page with the Top Navigation, check the following - Without the tool in the allow list - ENV does not have key `top_navigation_tools` - No tool buttons show up in the navigation bar - With the tool in the allow list by dev key Setting.set("top_navigation_allowed_dev_keys", "global-dev-key-id") - ENV.top_navigation_tools is populated with the tool config - The tool menu button shows up in the top navigation bar - The tool is listed in the menu and can be launched in drawer - Adjust the page to mobile width - Ensure the tool menu shows up in mobile header and can be launched - The tool should open in a tray that overlays the content - Repeat with the tool only in the domain list Setting.set("top_navigation_allowed_dev_keys", "") Setting.set("top_navigation_allowed_launch_domains", "tool-launch-domain") - With the tool in either allow list, add it to the pinned keys list Setting.set("top_navigation_pinned_dev_keys", "global-dev-key-id") - Ensure the tool has a dedicated button in top nav and the tool menu is not rendered. - Repeate with the tool only in the allowd domains list Setting.set("top_navigation_pinned_dev_keys", "") Setting.set("top_navigation_pinned_launch_domains", "tool-launch-domain") - With the tool in the pinned list but not the allowed list, ensure it is not rendered in the top navigation menu and is not in ENV. Change-Id: I267b78316e54dab7f811e3a7d8eaa752668ac5db Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/347352 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Xander Moffatt <xmoffatt@instructure.com> QA-Review: Xander Moffatt <xmoffatt@instructure.com> Product-Review: Dustin Cowles <dustin.cowles@instructure.com>
This commit is contained in:
parent
a3cc55f4f6
commit
c3e952a1a4
|
@ -19,6 +19,7 @@
|
|||
css_bundle(:instructure_eportfolio) if @eportfolio_view === true
|
||||
css_bundle(:new_user_tutorials) if tutorials_enabled?
|
||||
js_bundle(:navigation_header) unless @headers == false
|
||||
js_bundle(:top_navigation_tools) if @domain_root_account&.feature_enabled?(:top_navigation_placement)
|
||||
|
||||
load_blueprint_courses_ui
|
||||
@has_content_notices = load_content_notices
|
||||
|
@ -95,7 +96,7 @@
|
|||
<%# Flash messages must be outside of #application or they won't work in screenreaders with modals open. %>
|
||||
<%= render :partial => 'shared/static_notices' %>
|
||||
<%= render :partial => 'shared/flash_notices' %>
|
||||
<%if !!@domain_root_account&.feature_enabled?(:external_tool_drawer) %>
|
||||
<%if @domain_root_account&.feature_enabled?(:top_navigation_placement) %>
|
||||
<div id="drawer-layout-mount-point"></div>
|
||||
<% end %>
|
||||
<div id="application" class="ic-app">
|
||||
|
@ -135,6 +136,9 @@
|
|||
<% end %>
|
||||
|
||||
<div class="right-of-crumbs">
|
||||
<%if @domain_root_account&.feature_enabled?(:top_navigation_placement) %>
|
||||
<div id="top-nav-tools-mount-point"></div>
|
||||
<% end %>
|
||||
<% if tutorials_enabled? %>
|
||||
<div class="TutorialToggleHolder"></div>
|
||||
<% end %>
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
<% js_bundle :nav_tourpoints %>
|
||||
<% end %>
|
||||
|
||||
<% if !!@domain_root_account&.feature_enabled?(:top_navigation_placement) %>
|
||||
<% js_bundle :top_navigation_tools %>
|
||||
<% end %>
|
||||
|
||||
<%
|
||||
if k5_user?
|
||||
dashboard_title = t('Home')
|
||||
|
@ -63,6 +67,9 @@
|
|||
<% else %>
|
||||
<div class="mobile-header-space"></div>
|
||||
<% end %>
|
||||
<% if !!@domain_root_account&.feature_enabled?(:top_navigation_placement) %>
|
||||
<div id="mobile-top-nav-tools-mount-point"></div>
|
||||
<% end %>
|
||||
<% if show_blueprint_button? %>
|
||||
<button class="mobile-header-blueprint-button Button Button--icon-action-rev Button--large" aria-label="<%= t "Open Blueprint Sidebar" %>">
|
||||
<i class="icon-blueprint"></i>
|
||||
|
|
|
@ -45,7 +45,7 @@ describe "add_people" do
|
|||
# open the add people modal dialog
|
||||
f("a#addUsers").click
|
||||
expect(f(".addpeople")).to be_displayed
|
||||
expect(f("#application")).to have_attribute("aria-hidden", "true")
|
||||
expect(f("#drawer-layout-mount-point")).to have_attribute("aria-hidden", "true")
|
||||
|
||||
# can't click the 'login id' radio button directly, since it's covered
|
||||
# with inst-ui prettiness and selenium won't allow it.
|
||||
|
|
|
@ -165,7 +165,7 @@ describe "Tutorials" do
|
|||
it "the 'Cancel' button closes the End Course Set-up Tutorial modal", priority: "1" do
|
||||
get "/courses/#{@course.id}"
|
||||
fj("button:contains('Don\\'t Show Again')").click
|
||||
fj("span button:contains('Cancel')").click
|
||||
fj("span[role='dialog'] button:contains('Cancel')").click
|
||||
expect(f(".NewUserTutorialTray")).to be_displayed
|
||||
expect(driver).not_to contain_css("End Course Set-up Tutorial")
|
||||
end
|
||||
|
|
|
@ -278,7 +278,7 @@ describe "discussions" do
|
|||
f("[data-testid='grading-schemes-selector-option-#{grading_standard.id}']").click
|
||||
|
||||
f(".form-actions button[type=submit]").click
|
||||
fj("span:contains('Continue')").click
|
||||
fj(".ui-button-text:contains('Continue')").click
|
||||
a = DiscussionTopic.last.assignment
|
||||
expect(a.grading_standard_id).to eq grading_standard.id
|
||||
end
|
||||
|
|
|
@ -150,7 +150,9 @@ describe "discussions" do
|
|||
f('input[type=checkbox][name="assignment[set_assignment]"]').click
|
||||
f("#has_group_category").click
|
||||
f(%(span[data-testid="group-set-close"])).click
|
||||
f("#edit_discussion_form_buttons .btn-primary[type=submit]").click
|
||||
submit_button = f("#edit_discussion_form_buttons .btn-primary[type=submit]")
|
||||
scroll_into_view(submit_button)
|
||||
submit_button.click
|
||||
wait_for_ajaximations
|
||||
error_box = f("div[role='alert'] .error_text")
|
||||
expect(error_box.text).to eq "Please create a group set"
|
||||
|
|
|
@ -26,6 +26,9 @@ describe "Gradebook History Page" do
|
|||
include CustomScreenActions
|
||||
|
||||
before(:once) do
|
||||
# This does not currently work correctly with top_navigation_placement enabled
|
||||
# ADV-112 is open to address this issue
|
||||
Account.default.disable_feature!(:top_navigation_placement)
|
||||
gb_history_setup(50)
|
||||
end
|
||||
|
||||
|
|
|
@ -173,8 +173,8 @@ describe "SpeedGrader - rubrics" do
|
|||
wait_for_ajaximations
|
||||
scroll_into_view(".toggle_full_rubric")
|
||||
f("button.toggle_full_rubric").click
|
||||
fj("span:contains('Rockin\\''):visible").click
|
||||
fj("span:contains('You Learned'):visible").click
|
||||
hover_and_click("span:contains('Rockin\\''):visible")
|
||||
hover_and_click("span:contains('You Learned'):visible")
|
||||
scroll_into_view(".save_rubric_button")
|
||||
f("#rubric_holder button.save_rubric_button").click
|
||||
wait_for_ajaximations
|
||||
|
|
|
@ -150,7 +150,7 @@ describe "master courses sidebar" do
|
|||
blueprint_open_sidebar_button.click
|
||||
f("button#mcSyncHistoryBtn").click
|
||||
expect(f('span[aria-label="Sync History"]')).to be_displayed
|
||||
expect(f("#application")).to have_attribute("aria-hidden", "true")
|
||||
expect(f("#drawer-layout-mount-point")).to have_attribute("aria-hidden", "true")
|
||||
end
|
||||
|
||||
it "shows Unsynced Changes modal when button is clicked" do
|
||||
|
@ -160,7 +160,7 @@ describe "master courses sidebar" do
|
|||
f("button#mcUnsyncedChangesBtn").click
|
||||
wait_for_ajaximations
|
||||
expect(f('span[aria-label="Unsynced Changes"]')).to be_displayed
|
||||
expect(f("#application")).to have_attribute("aria-hidden", "true")
|
||||
expect(f("#drawer-layout-mount-point")).to have_attribute("aria-hidden", "true")
|
||||
end
|
||||
|
||||
it "does not show the tutorial sidebar button" do
|
||||
|
|
|
@ -164,7 +164,7 @@ describe "quiz restrictions as a teacher" do
|
|||
f("#quiz_ip_filter").send_keys("7")
|
||||
|
||||
wait_for_new_page_load { f("button.save_quiz_button.btn.btn-primary").click }
|
||||
expect(ffj(".error_text")[1]).to include_text("IP filter is not valid")
|
||||
expect(ff(".error_text")[-1]).to include_text("IP filter is not valid")
|
||||
end
|
||||
|
||||
it "has a working link to help with ip address filtering", priority: "1" do
|
||||
|
|
|
@ -23,8 +23,13 @@ module CustomScreenActions
|
|||
end
|
||||
|
||||
def scroll_page_to_bottom
|
||||
# If the drawer layout is in use, we need to scroll the content element instead
|
||||
if element_has_children?("#drawer-layout-mount-point")
|
||||
scroll_element("#drawer-layout-content", "max")
|
||||
else
|
||||
driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")
|
||||
end
|
||||
end
|
||||
|
||||
def resize_screen_to_standard
|
||||
driver.manage.window.maximize
|
||||
|
|
|
@ -208,6 +208,7 @@ const featureBundles: {
|
|||
terms_of_use: () => import('./features/terms_of_use/index'),
|
||||
theme_editor: () => import('./features/theme_editor/index'),
|
||||
theme_preview: () => import('./features/theme_preview/index'),
|
||||
top_navigation_tools: () => import('./features/top_navigation_tools/index'),
|
||||
user_grades: () => import('./features/user_grades/index'),
|
||||
user_lists: () => import('./features/user_lists/index'),
|
||||
user_logins: () => import('./features/user_logins/index'),
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ready from '@instructure/ready'
|
||||
import ContentTypeExternalToolDrawer from '@canvas/trays/react/ContentTypeExternalToolDrawer'
|
||||
import {TopNavigationTools, MobileTopNavigationTools} from './react/TopNavigationTools'
|
||||
import type {Tool} from '@canvas/global/env/EnvCommon'
|
||||
|
||||
const I18n = useI18nScope('common')
|
||||
|
||||
ready(() => {
|
||||
const drawerLayoutMountPoint = document.getElementById('drawer-layout-mount-point')
|
||||
const topNavToolsMountPoint = document.getElementById('top-nav-tools-mount-point')
|
||||
const mobileTopNavToolsMountPoint = document.getElementById('mobile-top-nav-tools-mount-point')
|
||||
const canvasApplicationBody = document.getElementById('application')
|
||||
let selectedTool: Tool | null = null
|
||||
|
||||
function handleDismissToolDrawer(): void {
|
||||
selectedTool = null
|
||||
renderExternalToolDrawer()
|
||||
}
|
||||
|
||||
function handleToolLaunch(tool: Tool): void {
|
||||
selectedTool = tool
|
||||
selectedTool.placement = 'top_navigation'
|
||||
renderExternalToolDrawer()
|
||||
}
|
||||
|
||||
function handleResize(): void {
|
||||
renderExternalToolDrawer()
|
||||
}
|
||||
|
||||
function renderExternalToolDrawer(): void {
|
||||
ReactDOM.render(
|
||||
<ContentTypeExternalToolDrawer
|
||||
tool={selectedTool}
|
||||
pageContent={canvasApplicationBody}
|
||||
pageContentTitle={I18n.t('Canvas LMS')}
|
||||
pageContentMinWidth="40rem"
|
||||
pageContentHeight={window.innerHeight}
|
||||
trayPlacement="end"
|
||||
onDismiss={handleDismissToolDrawer}
|
||||
onResize={handleResize}
|
||||
open={!!selectedTool}
|
||||
/>,
|
||||
drawerLayoutMountPoint
|
||||
)
|
||||
}
|
||||
|
||||
function renderTopNavigationTools(): void {
|
||||
ReactDOM.render(
|
||||
<TopNavigationTools tools={ENV.top_navigation_tools} handleToolLaunch={handleToolLaunch} />,
|
||||
topNavToolsMountPoint
|
||||
)
|
||||
|
||||
if (mobileTopNavToolsMountPoint) {
|
||||
ReactDOM.render(
|
||||
<MobileTopNavigationTools
|
||||
tools={ENV.top_navigation_tools}
|
||||
handleToolLaunch={handleToolLaunch}
|
||||
/>,
|
||||
mobileTopNavToolsMountPoint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (drawerLayoutMountPoint && topNavToolsMountPoint && canvasApplicationBody) {
|
||||
renderExternalToolDrawer()
|
||||
renderTopNavigationTools()
|
||||
}
|
||||
})
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@canvas-features/top_navigation_tools",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"owner": "FOO"
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {Button, IconButton} from '@instructure/ui-buttons'
|
||||
import {Menu} from '@instructure/ui-menu'
|
||||
import {IconLtiLine} from '@instructure/ui-icons'
|
||||
import {Img} from '@instructure/ui-img'
|
||||
import {TruncateText} from '@instructure/ui-truncate-text'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import type {Tool} from '@canvas/global/env/EnvCommon'
|
||||
|
||||
const I18n = useI18nScope('top_navigation_tools')
|
||||
|
||||
type TopNavigationToolsProps = {
|
||||
tools: Tool[]
|
||||
handleToolLaunch: (tool: Tool) => void // eslint-disable-line react/no-unused-prop-types
|
||||
}
|
||||
|
||||
function getToolIcon(tool: Tool) {
|
||||
return (
|
||||
(tool.icon_url && (
|
||||
<Img src={tool.icon_url} height="1rem" alt={tool.title || 'Tool Icon'} />
|
||||
)) || <IconLtiLine alt={tool.title || 'Tool Icon'} />
|
||||
)
|
||||
}
|
||||
|
||||
function handleToolClick(val: String, tools: Tool[], handleToolLaunch: (tool: Tool) => void) {
|
||||
const targeted_tool = tools.find((tool: Tool) => tool.id === val)
|
||||
if (targeted_tool) {
|
||||
handleToolLaunch(targeted_tool)
|
||||
}
|
||||
}
|
||||
|
||||
export function TopNavigationTools(props: TopNavigationToolsProps) {
|
||||
const pinned_tools = props.tools.filter(tool => tool.pinned)
|
||||
const menu_tools = props.tools.filter(tool => !tool.pinned)
|
||||
|
||||
return (
|
||||
<Flex as="div" direct="row" marginEnd="small" marginStart="small" gap="small">
|
||||
{pinned_tools.map((tool: Tool) => {
|
||||
return (
|
||||
<Flex.Item key={tool.id}>
|
||||
<Button
|
||||
renderIcon={getToolIcon(tool)}
|
||||
onClick={e =>
|
||||
handleToolClick(e.target.dataset.toolId, pinned_tools, props.handleToolLaunch)
|
||||
}
|
||||
data-tool-id={tool.id}
|
||||
>
|
||||
<TruncateText>{tool.title}</TruncateText>
|
||||
</Button>
|
||||
</Flex.Item>
|
||||
)
|
||||
})}
|
||||
{menu_tools.length > 0 && (
|
||||
<Flex.Item>
|
||||
<Menu placement="bottom end" trigger={<Button renderIcon={IconLtiLine} />} key="menu">
|
||||
{menu_tools.map((tool: Tool) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
onSelect={(e, val) => handleToolClick(val, menu_tools, props.handleToolLaunch)}
|
||||
key={tool.id}
|
||||
value={tool.id}
|
||||
label={I18n.t('Launch %{tool}', {tool: tool.title})}
|
||||
>
|
||||
<Flex direction="row" gap="small">
|
||||
{getToolIcon(tool)}
|
||||
<TruncateText>{tool.title}</TruncateText>
|
||||
</Flex>
|
||||
</Menu.Item>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
</Flex.Item>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileTopNavigationTools(props: TopNavigationToolsProps) {
|
||||
return (
|
||||
<Menu
|
||||
placement="bottom end"
|
||||
trigger={
|
||||
<IconButton
|
||||
renderIcon={IconLtiLine}
|
||||
screenReaderLabel={I18n.t('LTI Tool Menu')}
|
||||
withBorder={false}
|
||||
withBackground={false}
|
||||
/>
|
||||
}
|
||||
key="menu"
|
||||
>
|
||||
{props.tools.map((tool: Tool) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
onSelect={(e, val) => handleToolClick(val, props)}
|
||||
key={tool.id}
|
||||
value={tool.id}
|
||||
label={I18n.t('Launch %{tool}', {tool: tool.title})}
|
||||
>
|
||||
<Flex direction="row" gap="small">
|
||||
{getToolIcon(tool)}
|
||||
<TruncateText>{tool.title}</TruncateText>
|
||||
</Flex>
|
||||
</Menu.Item>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
import {TopNavigationTools, MobileTopNavigationTools} from '../TopNavigationTools'
|
||||
|
||||
describe('TopNavigationTools', () => {
|
||||
it('renders', () => {
|
||||
const tools = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tool 1',
|
||||
icon_url: 'https://instructure.com',
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tool 2',
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
const handleToolLaunch = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<TopNavigationTools tools={tools} handleToolLaunch={handleToolLaunch} />
|
||||
)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with no tools', () => {
|
||||
const tools = []
|
||||
const handleToolLaunch = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<TopNavigationTools tools={tools} handleToolLaunch={handleToolLaunch} />
|
||||
)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with no pinned tools', () => {
|
||||
const tools = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tool 1',
|
||||
icon_url: 'https://instructure.com',
|
||||
pinned: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tool 2',
|
||||
icon_url: 'https://instructure.com',
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
const handleToolLaunch = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<TopNavigationTools tools={tools} handleToolLaunch={handleToolLaunch} />
|
||||
)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('MobileTopNavigationTools', () => {
|
||||
it('renders', () => {
|
||||
const tools = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tool 1',
|
||||
icon_url: 'https://instructure.com',
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tool 2',
|
||||
icon_url: 'https://instructure.com',
|
||||
pinned: true,
|
||||
},
|
||||
]
|
||||
const handleToolLaunch = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<MobileTopNavigationTools tools={tools} handleToolLaunch={handleToolLaunch} />
|
||||
)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleToolClick', () => {
|
||||
it('finds tool', () => {
|
||||
const tool = {
|
||||
id: '1',
|
||||
title: 'Tool 1',
|
||||
icon_url: 'https://instructure.com',
|
||||
pinned: true,
|
||||
}
|
||||
const handleToolLaunch = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<TopNavigationTools tools={[tool]} handleToolLaunch={handleToolLaunch} />
|
||||
)
|
||||
wrapper.find('Button').simulate('click', {target: {dataset: {toolId: '1'}}})
|
||||
expect(handleToolLaunch).toHaveBeenCalledWith(tool)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,408 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MobileTopNavigationTools renders 1`] = `
|
||||
<Menu
|
||||
constrain="window"
|
||||
defaultShow={false}
|
||||
disabled={false}
|
||||
key="menu"
|
||||
label={null}
|
||||
mountNode={null}
|
||||
offsetX={0}
|
||||
offsetY={0}
|
||||
placement="bottom end"
|
||||
shouldFocusTriggerOnClose={true}
|
||||
shouldHideOnSelect={true}
|
||||
trigger={
|
||||
<IconButton
|
||||
as="button"
|
||||
color="secondary"
|
||||
cursor="pointer"
|
||||
margin="0"
|
||||
renderIcon={[Function]}
|
||||
screenReaderLabel="LTI Tool Menu"
|
||||
shape="rectangle"
|
||||
size="medium"
|
||||
type="button"
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
/>
|
||||
}
|
||||
withArrow={true}
|
||||
>
|
||||
<MenuItem
|
||||
disabled={false}
|
||||
key="1"
|
||||
label="Launch Tool 1"
|
||||
onSelect={[Function]}
|
||||
type="button"
|
||||
value="1"
|
||||
>
|
||||
<Flex
|
||||
as="span"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Img
|
||||
alt="Tool 1"
|
||||
display="inline-block"
|
||||
height="1rem"
|
||||
src="https://instructure.com"
|
||||
withBlur={false}
|
||||
withGrayscale={false}
|
||||
/>
|
||||
<TruncateText
|
||||
debounce={0}
|
||||
ellipsis="…"
|
||||
ignore={
|
||||
[
|
||||
" ",
|
||||
".",
|
||||
",",
|
||||
]
|
||||
}
|
||||
maxLines={1}
|
||||
position="end"
|
||||
truncate="character"
|
||||
>
|
||||
Tool 1
|
||||
</TruncateText>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={false}
|
||||
key="2"
|
||||
label="Launch Tool 2"
|
||||
onSelect={[Function]}
|
||||
type="button"
|
||||
value="2"
|
||||
>
|
||||
<Flex
|
||||
as="span"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Img
|
||||
alt="Tool 2"
|
||||
display="inline-block"
|
||||
height="1rem"
|
||||
src="https://instructure.com"
|
||||
withBlur={false}
|
||||
withGrayscale={false}
|
||||
/>
|
||||
<TruncateText
|
||||
debounce={0}
|
||||
ellipsis="…"
|
||||
ignore={
|
||||
[
|
||||
" ",
|
||||
".",
|
||||
",",
|
||||
]
|
||||
}
|
||||
maxLines={1}
|
||||
position="end"
|
||||
truncate="character"
|
||||
>
|
||||
Tool 2
|
||||
</TruncateText>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
`;
|
||||
|
||||
exports[`TopNavigationTools renders 1`] = `
|
||||
<Flex
|
||||
as="div"
|
||||
direct="row"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
marginEnd="small"
|
||||
marginStart="small"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Item
|
||||
as="span"
|
||||
key="1"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
>
|
||||
<Button
|
||||
as="button"
|
||||
color="secondary"
|
||||
cursor="pointer"
|
||||
data-tool-id="1"
|
||||
display="inline-block"
|
||||
margin="0"
|
||||
onClick={[Function]}
|
||||
renderIcon={
|
||||
<Img
|
||||
alt="Tool 1"
|
||||
display="inline-block"
|
||||
height="1rem"
|
||||
src="https://instructure.com"
|
||||
withBlur={false}
|
||||
withGrayscale={false}
|
||||
/>
|
||||
}
|
||||
size="medium"
|
||||
textAlign="center"
|
||||
type="button"
|
||||
withBackground={true}
|
||||
>
|
||||
<TruncateText
|
||||
debounce={0}
|
||||
ellipsis="…"
|
||||
ignore={
|
||||
[
|
||||
" ",
|
||||
".",
|
||||
",",
|
||||
]
|
||||
}
|
||||
maxLines={1}
|
||||
position="end"
|
||||
truncate="character"
|
||||
>
|
||||
Tool 1
|
||||
</TruncateText>
|
||||
</Button>
|
||||
</Item>
|
||||
<Item
|
||||
as="span"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
>
|
||||
<Menu
|
||||
constrain="window"
|
||||
defaultShow={false}
|
||||
disabled={false}
|
||||
key="menu"
|
||||
label={null}
|
||||
mountNode={null}
|
||||
offsetX={0}
|
||||
offsetY={0}
|
||||
placement="bottom end"
|
||||
shouldFocusTriggerOnClose={true}
|
||||
shouldHideOnSelect={true}
|
||||
trigger={
|
||||
<Button
|
||||
as="button"
|
||||
color="secondary"
|
||||
cursor="pointer"
|
||||
display="inline-block"
|
||||
margin="0"
|
||||
renderIcon={[Function]}
|
||||
size="medium"
|
||||
textAlign="center"
|
||||
type="button"
|
||||
withBackground={true}
|
||||
/>
|
||||
}
|
||||
withArrow={true}
|
||||
>
|
||||
<MenuItem
|
||||
disabled={false}
|
||||
key="2"
|
||||
label="Launch Tool 2"
|
||||
onSelect={[Function]}
|
||||
type="button"
|
||||
value="2"
|
||||
>
|
||||
<Flex
|
||||
as="span"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<IconLtiLine
|
||||
alt="Tool 2"
|
||||
/>
|
||||
<TruncateText
|
||||
debounce={0}
|
||||
ellipsis="…"
|
||||
ignore={
|
||||
[
|
||||
" ",
|
||||
".",
|
||||
",",
|
||||
]
|
||||
}
|
||||
maxLines={1}
|
||||
position="end"
|
||||
truncate="character"
|
||||
>
|
||||
Tool 2
|
||||
</TruncateText>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Item>
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`TopNavigationTools renders with no pinned tools 1`] = `
|
||||
<Flex
|
||||
as="div"
|
||||
direct="row"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
marginEnd="small"
|
||||
marginStart="small"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Item
|
||||
as="span"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
>
|
||||
<Menu
|
||||
constrain="window"
|
||||
defaultShow={false}
|
||||
disabled={false}
|
||||
key="menu"
|
||||
label={null}
|
||||
mountNode={null}
|
||||
offsetX={0}
|
||||
offsetY={0}
|
||||
placement="bottom end"
|
||||
shouldFocusTriggerOnClose={true}
|
||||
shouldHideOnSelect={true}
|
||||
trigger={
|
||||
<Button
|
||||
as="button"
|
||||
color="secondary"
|
||||
cursor="pointer"
|
||||
display="inline-block"
|
||||
margin="0"
|
||||
renderIcon={[Function]}
|
||||
size="medium"
|
||||
textAlign="center"
|
||||
type="button"
|
||||
withBackground={true}
|
||||
/>
|
||||
}
|
||||
withArrow={true}
|
||||
>
|
||||
<MenuItem
|
||||
disabled={false}
|
||||
key="1"
|
||||
label="Launch Tool 1"
|
||||
onSelect={[Function]}
|
||||
type="button"
|
||||
value="1"
|
||||
>
|
||||
<Flex
|
||||
as="span"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Img
|
||||
alt="Tool 1"
|
||||
display="inline-block"
|
||||
height="1rem"
|
||||
src="https://instructure.com"
|
||||
withBlur={false}
|
||||
withGrayscale={false}
|
||||
/>
|
||||
<TruncateText
|
||||
debounce={0}
|
||||
ellipsis="…"
|
||||
ignore={
|
||||
[
|
||||
" ",
|
||||
".",
|
||||
",",
|
||||
]
|
||||
}
|
||||
maxLines={1}
|
||||
position="end"
|
||||
truncate="character"
|
||||
>
|
||||
Tool 1
|
||||
</TruncateText>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={false}
|
||||
key="2"
|
||||
label="Launch Tool 2"
|
||||
onSelect={[Function]}
|
||||
type="button"
|
||||
value="2"
|
||||
>
|
||||
<Flex
|
||||
as="span"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Img
|
||||
alt="Tool 2"
|
||||
display="inline-block"
|
||||
height="1rem"
|
||||
src="https://instructure.com"
|
||||
withBlur={false}
|
||||
withGrayscale={false}
|
||||
/>
|
||||
<TruncateText
|
||||
debounce={0}
|
||||
ellipsis="…"
|
||||
ignore={
|
||||
[
|
||||
" ",
|
||||
".",
|
||||
",",
|
||||
]
|
||||
}
|
||||
maxLines={1}
|
||||
position="end"
|
||||
truncate="character"
|
||||
>
|
||||
Tool 2
|
||||
</TruncateText>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Item>
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`TopNavigationTools renders with no tools 1`] = `
|
||||
<Flex
|
||||
as="div"
|
||||
direct="row"
|
||||
direction="row"
|
||||
display="flex"
|
||||
gap="small"
|
||||
justifyItems="start"
|
||||
marginEnd="small"
|
||||
marginStart="small"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
/>
|
||||
`;
|
|
@ -41,6 +41,17 @@ type Role = {
|
|||
plural_label: string
|
||||
}
|
||||
|
||||
type ToolPlacement = 'top_navigation'
|
||||
|
||||
export type Tool = {
|
||||
id: string
|
||||
title: string
|
||||
base_url: string
|
||||
icon_url: string
|
||||
pinned?: boolean
|
||||
placement?: ToolPlacement
|
||||
}
|
||||
|
||||
export type GroupOutcome = {
|
||||
id: string
|
||||
title: string
|
||||
|
@ -210,6 +221,12 @@ export interface EnvCommon {
|
|||
classes?: string
|
||||
}>
|
||||
breadcrumbs: {name: string; url: string}[]
|
||||
|
||||
/**
|
||||
* Used by ui/features/top_navigation_tools/react/TopNavigationTools.tsx
|
||||
* and ui/shared/trays/react/ContentTypeExternalToolDrawer.tsx
|
||||
*/
|
||||
top_navigation_tools: Tool[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,9 @@ import parseQuestionId from '../util/parse_question_id'
|
|||
|
||||
export default class QuestionViewed extends EventTracker {
|
||||
install(deliver, scrollContainer = window) {
|
||||
// The quiz might be inside a drawer layout, in which case we need to
|
||||
// watch the content inside the drawer layout instead of the window.
|
||||
scrollContainer = document.getElementById('drawer-layout-content') || scrollContainer
|
||||
let viewed = []
|
||||
|
||||
return this.bind(
|
||||
|
|
|
@ -27,47 +27,19 @@ import {TruncateText} from '@instructure/ui-truncate-text'
|
|||
import {View} from '@instructure/ui-view'
|
||||
import {handleExternalContentMessages} from '@canvas/external-tools/messages'
|
||||
import ToolLaunchIframe from '@canvas/external-tools/react/components/ToolLaunchIframe'
|
||||
|
||||
type Tool = {
|
||||
id: string
|
||||
title: string
|
||||
base_url: string
|
||||
icon_url: string
|
||||
}
|
||||
|
||||
type KnownResourceType =
|
||||
| 'assignment'
|
||||
| 'assignment_group'
|
||||
| 'audio'
|
||||
| 'discussion_topic'
|
||||
| 'document'
|
||||
| 'image'
|
||||
| 'module'
|
||||
| 'quiz'
|
||||
| 'page'
|
||||
| 'video'
|
||||
|
||||
export type SelectableItem = {
|
||||
course_id: string
|
||||
type: KnownResourceType
|
||||
}
|
||||
import type {Tool} from '@canvas/global/env/EnvCommon'
|
||||
|
||||
type Props = {
|
||||
tool: Tool
|
||||
tool: Tool | null
|
||||
pageContent: Element
|
||||
pageContentTitle: string
|
||||
pageContentMinWidth: string
|
||||
pageContentHeight: string
|
||||
trayPlacement: string
|
||||
acceptedResourceTypes: KnownResourceType[]
|
||||
targetResourceType: KnownResourceType
|
||||
allowItemSelection: boolean
|
||||
selectableItems: SelectableItem[]
|
||||
onDismiss: any
|
||||
onExternalContentReady: any
|
||||
onResize: any
|
||||
onExternalContentReady?: any
|
||||
open: boolean
|
||||
placement: string
|
||||
extraQueryParams?: {}
|
||||
}
|
||||
|
||||
export default function ContentTypeExternalToolDrawer({
|
||||
|
@ -77,30 +49,17 @@ export default function ContentTypeExternalToolDrawer({
|
|||
pageContentMinWidth,
|
||||
pageContentHeight,
|
||||
trayPlacement,
|
||||
acceptedResourceTypes,
|
||||
targetResourceType,
|
||||
allowItemSelection,
|
||||
selectableItems,
|
||||
onDismiss,
|
||||
onResize,
|
||||
onExternalContentReady,
|
||||
open,
|
||||
placement,
|
||||
extraQueryParams = {},
|
||||
}: Props) {
|
||||
const queryParams = {
|
||||
com_instructure_course_accept_canvas_resource_types: acceptedResourceTypes,
|
||||
com_instructure_course_canvas_resource_type: targetResourceType,
|
||||
com_instructure_course_allow_canvas_resource_selection: allowItemSelection,
|
||||
com_instructure_course_available_canvas_resources: selectableItems,
|
||||
display: 'borderless',
|
||||
placement,
|
||||
...extraQueryParams,
|
||||
}
|
||||
const queryParams = tool ? {display: 'borderless', placement: tool.placement} : {}
|
||||
const prefix = tool?.base_url.indexOf('?') === -1 ? '?' : '&'
|
||||
const iframeUrl = `${tool?.base_url}${prefix}${$.param(queryParams)}`
|
||||
const toolTitle = tool ? tool.title : 'External Tool'
|
||||
const toolIconUrl = tool ? tool.icon_url : ''
|
||||
const toolIconAlt = toolTitle ? `${toolTitle} icon` : ''
|
||||
const toolIconUrl = tool?.icon_url
|
||||
const toolIconAlt = toolTitle ? `${toolTitle} Icon` : 'Tool Icon'
|
||||
const iframeRef = useRef()
|
||||
const pageContentRef = useRef()
|
||||
|
||||
|
@ -115,6 +74,14 @@ export default function ContentTypeExternalToolDrawer({
|
|||
[pageContent]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
}
|
||||
}, [onResize])
|
||||
|
||||
useEffect(
|
||||
// returns cleanup function:
|
||||
() => handleExternalContentMessages({ready: onExternalContentReady}),
|
||||
|
@ -124,7 +91,7 @@ export default function ContentTypeExternalToolDrawer({
|
|||
return (
|
||||
<View display="block" height={pageContentHeight}>
|
||||
<DrawerLayout minWidth={pageContentMinWidth}>
|
||||
<DrawerLayout.Content label={pageContentTitle}>
|
||||
<DrawerLayout.Content label={pageContentTitle} id="drawer-layout-content">
|
||||
<div ref={pageContentRef} />
|
||||
</DrawerLayout.Content>
|
||||
<DrawerLayout.Tray
|
||||
|
|
|
@ -21,7 +21,13 @@ import {render, fireEvent} from '@testing-library/react'
|
|||
import ContentTypeExternalToolDrawer from '../ContentTypeExternalToolDrawer'
|
||||
|
||||
describe('ContentTypeExternalToolDrawer', () => {
|
||||
const tool = {id: '1', base_url: 'https://one.lti.com/', title: 'First LTI'}
|
||||
const tool = {
|
||||
id: '1',
|
||||
base_url: 'https://lti1.example.com/',
|
||||
title: 'First LTI',
|
||||
pinned: true,
|
||||
placement: 'top_navigation',
|
||||
}
|
||||
const onDismiss = jest.fn()
|
||||
const onExternalContentReady = jest.fn()
|
||||
const extraQueryParams = {key1: 'value1', key2: 'value2'}
|
||||
|
@ -39,17 +45,11 @@ describe('ContentTypeExternalToolDrawer', () => {
|
|||
tool={tool}
|
||||
pageContent={pageContent}
|
||||
pageContentTitle={pageContentTitle}
|
||||
pageContentMinWidth=""
|
||||
pageContentMinWidth="40rem"
|
||||
trayPlacement="end"
|
||||
acceptedResourceTypes={['page', 'module']}
|
||||
targetResourceType="page"
|
||||
allowItemSelection={true}
|
||||
selectableItems={[{id: '1', name: 'module 1'}]}
|
||||
onDismiss={onDismiss}
|
||||
onExternalContentReady={onExternalContentReady}
|
||||
open={true}
|
||||
placement="wiki_index_menu"
|
||||
extraQueryParams={extraQueryParams}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
@ -75,7 +75,7 @@ describe('ContentTypeExternalToolDrawer', () => {
|
|||
let icon_url
|
||||
|
||||
beforeAll(() => {
|
||||
icon_url = 'https://one.lti.com/icon.png'
|
||||
icon_url = 'https://lti1.example.com/icon.png'
|
||||
tool.icon_url = icon_url
|
||||
})
|
||||
|
||||
|
@ -85,7 +85,7 @@ describe('ContentTypeExternalToolDrawer', () => {
|
|||
|
||||
it('renders an icon', () => {
|
||||
const {getByAltText} = renderTray()
|
||||
expect(getByAltText('First LTI icon')).toHaveAttribute('src', icon_url)
|
||||
expect(getByAltText('First LTI Icon')).toHaveAttribute('src', icon_url)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -103,33 +103,10 @@ describe('ContentTypeExternalToolDrawer', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('constructs iframe src url', () => {
|
||||
it('adds ? before parameters if none are already present', () => {
|
||||
it('constructs iframe src url', () => {
|
||||
expect(tool.base_url).not.toContain('?')
|
||||
const {getByTestId} = renderTray()
|
||||
const src = getByTestId('ltiIframe').src
|
||||
expect(src).toContain(`${tool.base_url}?`)
|
||||
})
|
||||
|
||||
it('appends parameters if some exist already', () => {
|
||||
tool.base_url = 'https://one.lti.com/?launch_type=wiki_index_menu'
|
||||
const {getByTestId} = renderTray()
|
||||
const src = getByTestId('ltiIframe').src
|
||||
expect(src).toContain(`${tool.base_url}&`)
|
||||
})
|
||||
|
||||
it('includes expected parameters', () => {
|
||||
const {getByTestId} = renderTray()
|
||||
const src = getByTestId('ltiIframe').src
|
||||
expect(src).toContain('com_instructure_course_accept_canvas_resource_types')
|
||||
expect(src).toContain('com_instructure_course_canvas_resource_type')
|
||||
expect(src).toContain('com_instructure_course_allow_canvas_resource_selection')
|
||||
expect(src).toContain('com_instructure_course_available_canvas_resources')
|
||||
expect(src).toContain('display')
|
||||
expect(src).toContain('placement')
|
||||
// from extraQueryParams
|
||||
expect(src).toContain('key1=value1')
|
||||
expect(src).toContain('key2=value2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue