add module_index_menu LTI placement

test plan:
* configure a tool with an module_index_menu placement
 (similar to the wiki_index_menu type)
* enable the "Import Commons Favorites" feature
* should launch the tool though a cog dropdown
 in the header of the modules page into a tray
* closing the tray after a message has been posted
 should cause the modules page to refresh

flag=commons_favorites
closes #LA-71 #LA-72

Change-Id: I4ab15bf71da574482b107cbbba295cb4557f4fa8
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/217828
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Tested-by: Jenkins
Reviewed-by: Carl Kibler <ckibler@instructure.com>
QA-Review: Carl Kibler <ckibler@instructure.com>
Product-Review: James Williams <jamesw@instructure.com>
This commit is contained in:
James Williams 2019-11-19 13:01:55 -07:00
parent 7d7214175b
commit 6990a780e4
14 changed files with 145 additions and 12 deletions

View File

@ -26,7 +26,7 @@ import itemView from './WikiPageIndexItemView'
import template from 'jst/wiki/WikiPageIndex'
import StickyHeaderMixin from '../StickyHeaderMixin'
import splitAssetString from '../../str/splitAssetString'
import ContentTypeExternalToolTray from './ContentTypeExternalToolTray'
import ContentTypeExternalToolTray from 'jsx/shared/ContentTypeExternalToolTray'
import DirectShareCourseTray from 'jsx/shared/direct_share/DirectShareCourseTray'
import DirectShareUserModal from 'jsx/shared/direct_share/DirectShareUserModal'
import 'jquery.disableWhileLoading'

View File

@ -75,6 +75,8 @@ class ContextModulesController < ApplicationController
:root_account => @domain_root_account, :current_user => @current_user).to_a
placements.select { |p| @menu_tools[p] = tools.select{|t| t.has_placement? p} }
@module_index_tools = @domain_root_account&.feature_enabled?(:commons_favorites) ? external_tools_display_hashes(:module_index_menu) : []
module_file_details = load_module_file_details if @context.grants_right?(@current_user, session, :manage_content)
js_env :course_id => @context.id,
:CONTEXT_URL_ROOT => polymorphic_path([@context]),
@ -84,7 +86,8 @@ class ContextModulesController < ApplicationController
:MODULE_FILE_PERMISSIONS => {
usage_rights_required: @context.usage_rights_required?,
manage_files: @context.grants_right?(@current_user, session, :manage_files)
}
},
:MODULE_INDEX_TOOLS => @module_index_tools
is_master_course = MasterCourses::MasterTemplate.is_master_course?(@context)
is_child_course = MasterCourses::ChildSubscription.is_child_course?(@context)

View File

@ -44,7 +44,8 @@ module ContextExternalToolsHelper
end
link_attrs = {
href: tool[:base_url]
:href => tool[:base_url],
"data-tool-id" => tool[:id]
}
link_attrs[:class] = options[:link_class] if options[:link_class]

View File

@ -93,6 +93,7 @@ export default class ExternalToolPlacementButton extends React.Component {
link_selection: I18n.t('Link Selection'),
migration_selection: I18n.t('Migration Selection'),
module_menu: I18n.t('Module Menu'),
module_index_menu: I18n.t('Modules Index Menu'),
post_grades: I18n.t('Sync Grades'),
quiz_menu: I18n.t('Quiz Menu'),
student_context_card: I18n.t('Student Context Card'),

View File

@ -34,6 +34,11 @@ const toolShape = shape({
icon_url: string
})
const moduleShape = shape({
id: string.isRequired,
name: string.isRequired
})
const knownResourceTypes = [
'assignment',
'assignment_group',
@ -53,7 +58,7 @@ ContentTypeExternalToolTray.propTypes = {
acceptedResourceTypes: arrayOf(oneOf(knownResourceTypes)).isRequired,
targetResourceType: oneOf(knownResourceTypes).isRequired,
allowItemSelection: bool.isRequired,
selectableItems: arrayOf(oneOf(knownResourceTypes)).isRequired,
selectableItems: arrayOf(moduleShape).isRequired,
onDismiss: func,
open: bool
}

View File

@ -18,7 +18,7 @@
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import ContentTypeExternalToolTray from 'compiled/views/wiki/ContentTypeExternalToolTray'
import ContentTypeExternalToolTray from '../ContentTypeExternalToolTray'
describe('ContentTypeExternalToolTray', () => {
const tool = {id: 1, base_url: 'https://one.lti.com/', title: 'First LTI'}

View File

@ -52,6 +52,7 @@ module Lti
:link_selection,
:migration_selection,
:module_menu,
:module_index_menu,
:post_grades,
:quiz_menu,
:resource_selection,

View File

@ -61,6 +61,18 @@
<%= t('#context_modules.buttons.add_module', 'Module') %>
</button>
<% end %>
<% if @module_index_tools.present? %>
<div class="inline-block module_index_tools">
<a class="al-trigger btn" role="button" aria-haspopup="true" aria-owns="toolbar-1" href="#">
<i class="icon-more" aria-hidden="true"></i>
<span class="screenreader-only"><%= t("Modules Settings") %></span>
</a>
<ul id="toolbar-1" class="al-options" role="menu" aria-hidden="true" aria-expanded="false">
<%= external_tools_menu_items(@module_index_tools, {link_class: "menu_tool_link", settings_key: :module_index_menu, in_list: true}) %>
</ul>
</div>
<% end %>
</div>
<% if @last_web_export %>
<small class="muted">
@ -73,6 +85,7 @@
</div>
</div>
<div id="external-tool-mount-point"></div>
<div class="item-group-container" id="context_modules_sortable_container">
<div id="no_context_modules_message" style="display:none;">
<% if can_do(@context.context_modules.temp_record, @current_user, :create) %>

View File

@ -139,7 +139,7 @@ module Lti
collaboration_params
when 'homework_submission'
homework_submission_params(assignment)
when 'wiki_index_menu'
when 'wiki_index_menu', 'module_index_menu'
{}
else
# TODO: we _could_, if configured, have any other placements return to the content migration page...

View File

@ -1304,17 +1304,20 @@ module Lti
USAGE_RIGHTS_GUARD
# Returns the types of resources that can be imported to the current page, forwarded from the request.
# Value is an array of one or more values of: ["assignment", "assignment_group", "audio",
# Value is a comma-separated array of one or more values of: ["assignment", "assignment_group", "audio",
# "discussion_topic", "document", "image", "module", "quiz", "page", "video"]
#
# @example
# ```
# ["page"]
# ["module"]
# ["assignment", "discussion_topic", "page", "quiz", "module"]
# "page"
# "module"
# "assignment,discussion_topic,page,quiz,module"
# ```
register_expansion 'com.instructure.Course.accept_canvas_resource_types', [],
-> { @request.parameters['com_instructure_course_accept_canvas_resource_types'] },
-> {
val = @request.parameters['com_instructure_course_accept_canvas_resource_types']
val.is_a?(Array) ? val.join(",") : val
},
default_name: 'com_instructure_course_accept_canvas_resource_types'
# Returns the target resource type for the current page, forwarded from the request.

View File

@ -40,6 +40,9 @@ import Publishable from 'compiled/models/Publishable'
import PublishButtonView from 'compiled/views/PublishButtonView'
import htmlEscape from './str/htmlEscape'
import setupContentIds from 'jsx/modules/utils/setupContentIds'
import ContentTypeExternalToolTray from 'jsx/shared/ContentTypeExternalToolTray'
import {ltiState} from './lti/post_message/handleLtiPostMessage'
import {monitorLtiMessages} from 'lti/messages'
import get from 'lodash/get'
import axios from 'axios'
import {showFlashError} from 'jsx/shared/FlashAlert'
@ -2500,6 +2503,63 @@ $(document).ready(function() {
$contextModules.each(function() {
modules.updateProgressionState($(this))
})
function setExternalToolTray(tool, returnFocusTo) {
const handleDismiss = () => {
setExternalToolTray(null)
returnFocusTo.focus()
if (ltiState?.tray?.refreshOnClose) {
window.location.reload()
}
}
const moduleData = []
$('#context_modules .context_module').each(function() {
moduleData.push({
id: $(this)
.attr('id')
.substring('context_module_'.length),
name: $(this)
.find('.name')
.attr('title')
})
})
ReactDOM.render(
<ContentTypeExternalToolTray
tool={tool}
placement="module_index_menu"
acceptedResourceTypes={[
'assignment',
'audio',
'discussion_topic',
'document',
'image',
'module',
'quiz',
'page',
'video'
]}
targetResourceType="module"
allowItemSelection
selectableItems={moduleData}
onDismiss={handleDismiss}
open={tool !== null}
/>,
$('#external-tool-mount-point')[0]
)
}
function openExternalTool(ev) {
if (ev != null) {
ev.preventDefault()
}
const tool = ENV.MODULE_INDEX_TOOLS.find(t => t.id === ev.target.dataset.toolId)
setExternalToolTray(tool, $('.al-trigger')[0])
}
$('.module_index_tools .menu_tool_link').click(openExternalTool)
monitorLtiMessages()
})
export default modules

View File

@ -545,6 +545,7 @@ describe ExternalToolsController, type: :request do
et.discussion_topic_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "discussion topic menu", display_type: 'full_width', visibility: 'admins'}
et.file_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "module menu", display_type: 'full_width', visibility: 'admins'}
et.module_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "module menu", display_type: 'full_width', visibility: 'admins'}
et.module_index_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "modules index menu", display_type: 'full_width', visibility: 'admins'}
et.quiz_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "quiz menu", display_type: 'full_width', visibility: 'admins'}
et.wiki_page_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "wiki page menu", display_type: 'full_width', visibility: 'admins'}
et.wiki_index_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "wiki index menu", display_type: 'full_width', visibility: 'admins'}
@ -722,6 +723,14 @@ describe ExternalToolsController, type: :request do
"display_type"=>'full_width',
"selection_height"=>400,
"selection_width"=>800},
"module_index_menu"=>
{"text"=>"modules index menu",
"label"=>"modules index menu",
"url"=>"http://www.example.com/ims/lti/resource",
"visibility"=>'admins',
"display_type"=>'full_width',
"selection_height"=>400,
"selection_width"=>800},
"quiz_menu"=>
{"text"=>"quiz menu",
"label"=>"quiz menu",

View File

@ -675,7 +675,7 @@ module Lti
it 'has substitution for $com.instructure.Course.accept_canvas_resource_types' do
exp_hash = {test: '$com.instructure.Course.accept_canvas_resource_types'}
variable_expander.expand_variables!(exp_hash)
expect(exp_hash[:test]).to eq ["page", "module"]
expect(exp_hash[:test]).to eq "page,module"
end
it 'has substitution for $com.instructure.Course.canvas_resource_type' do

View File

@ -168,4 +168,41 @@ describe "context modules" do
expect(link).not_to be_displayed
end
end
context "module index tool placement" do
before do
course_with_teacher_logged_in
@tool = Account.default.context_external_tools.new(:name => "a", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.module_index_menu = {:url => "http://www.example.com", :text => "Import Stuff"}
@tool.save!
@module1 = @course.context_modules.create!(:name => "module1")
@module2 = @course.context_modules.create!(:name => "module2")
Account.default.enable_feature!(:commons_favorites)
end
it "should be able to launch the index menu tool via the tray", custom_timeout: 60 do
get "/courses/#{@course.id}/modules"
gear = f(".header-bar .al-trigger")
gear.click
tool_link = f(".header-bar li.ui-menu-item a.menu_tool_link")
expect(tool_link).to include_text("Import Stuff")
tool_link.click
wait_for_ajaximations
tray = f("[role='dialog']")
expect(tray['aria-label']).to eq "Import Stuff"
iframe = tray.find_element(:css, "iframe")
expect(iframe['src']).to include("/courses/#{@course.id}/external_tools/#{@tool.id}")
query_params = Rack::Utils.parse_nested_query(URI.parse(iframe['src']).query)
expect(query_params["launch_type"]).to eq "module_index_menu"
expect(query_params["com_instructure_course_allow_canvas_resource_selection"]).to eq "true"
expect(query_params["com_instructure_course_accept_canvas_resource_types"]).to match_array(
["assignment", "audio", "discussion_topic", "document", "image", "module", "quiz", "page", "video"])
module_data = [@module1, @module2].map{|m| {"id" => m.id.to_s, "name" => m.name}}
expect(query_params["com_instructure_course_available_canvas_resources"].values).to match_array(module_data)
end
end
end