Add LTI configuration placement (course and account)
Fixes PLAT-843
Test steps:
- Add a new LTI tool with the following URL:
1f62b5221c/tool%20setting.xml
- Once it's added, a button with a gear should appear next to it.
- Click the button and ensure that the iframe launches in a modal
- Do this in both account and course
Change-Id: I637812efecce22f178e4991c3d07646860365329
Reviewed-on: https://gerrit.instructure.com/47553
Tested-by: Jenkins
Reviewed-by: Clay Diffrient <cdiffrient@instructure.com>
QA-Review: Bracken Mosbacker <bracken@instructure.com>
Product-Review: Brad Humphrey <brad@instructure.com>
This commit is contained in:
parent
3d5c10e55f
commit
ca2187815e
|
@ -540,7 +540,7 @@ class AccountsController < ApplicationController
|
|||
APP_CENTER: { enabled: Canvas::Plugin.find(:app_center).enabled? },
|
||||
ENABLE_LTI2: @account.root_account.feature_enabled?(:lti2_ui),
|
||||
LTI_LAUNCH_URL: account_tool_proxy_registration_path(@account),
|
||||
CONTEXT_BASE_URL: "/api/v1/accounts/#{@context.id}"
|
||||
CONTEXT_BASE_URL: "/accounts/#{@context.id}"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1002,7 +1002,7 @@ class CoursesController < ApplicationController
|
|||
},
|
||||
ENABLE_LTI2: @domain_root_account.feature_enabled?(:lti2_ui),
|
||||
LTI_LAUNCH_URL: course_tool_proxy_registration_path(@context),
|
||||
CONTEXT_BASE_URL: "/api/v1/courses/#{@context.id}"
|
||||
CONTEXT_BASE_URL: "/courses/#{@context.id}"
|
||||
})
|
||||
|
||||
@course_settings_sub_navigation_tools = ContextExternalTool.all_tools_for(@context, :type => :course_settings_sub_navigation, :root_account => @domain_root_account, :current_user => @current_user)
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/** @jsx React.DOM */
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'i18n!external_tools',
|
||||
'react',
|
||||
'react-modal'
|
||||
], function ($, I18n, React, Modal) {
|
||||
|
||||
return React.createClass({
|
||||
displayName: 'ConfigureExternalToolButton',
|
||||
|
||||
propTypes: {
|
||||
tool: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
modalIsOpen: false
|
||||
}
|
||||
},
|
||||
|
||||
openModal(e) {
|
||||
e.preventDefault();
|
||||
this.setState({modalIsOpen: true});
|
||||
},
|
||||
|
||||
closeModal(cb) {
|
||||
if (typeof cb === 'function') {
|
||||
this.setState({modalIsOpen: false}, cb);
|
||||
} else {
|
||||
this.setState({modalIsOpen: false});
|
||||
}
|
||||
},
|
||||
|
||||
getLaunchUrl() {
|
||||
var toolConfigUrl = this.props.tool.tool_configuration.url;
|
||||
return ENV.CONTEXT_BASE_URL + '/external_tools/retrieve?url=' + encodeURIComponent(toolConfigUrl) + '&display=borderless';
|
||||
},
|
||||
|
||||
renderIframe() {
|
||||
if (this.state.modalIsOpen) {
|
||||
return <iframe src={this.getLaunchUrl()} style={{
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: 500,
|
||||
border: 0
|
||||
}}/>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className="ConfigureExternalToolButton">
|
||||
<a href="#" ref="btnTriggerModal" role="button" aria-label={I18n.t('Configure %{toolName} App', { toolName: this.props.tool.name })} className="lm" onClick={this.openModal}>
|
||||
<i className="icon-settings btn"></i>
|
||||
</a>
|
||||
<Modal className="ReactModal__Content--canvas"
|
||||
overlayClassName="ReactModal__Overlay--canvas"
|
||||
isOpen={this.state.modalIsOpen}
|
||||
onRequestClose={this.closeModal}>
|
||||
|
||||
<div className="ReactModal__Layout">
|
||||
|
||||
<div className="ReactModal__InnerSection ReactModal__Header ReactModal__Header--force-no-corners">
|
||||
<div className="ReactModal__Header-Title">
|
||||
<h4>{I18n.t('Configurate %{tool} App?', {tool: this.props.tool.name})}</h4>
|
||||
</div>
|
||||
<div className="ReactModal__Header-Actions">
|
||||
<button className="Button Button--icon-action" type="button" onClick={this.closeModal}>
|
||||
<i className="icon-x"></i>
|
||||
<span className="screenreader-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ReactModal__InnerSection ReactModal__Body ReactModal__Body--force-no-padding">
|
||||
{this.renderIframe()}
|
||||
</div>
|
||||
|
||||
<div className="ReactModal__InnerSection ReactModal__Footer">
|
||||
<div className="ReactModal__Footer-Actions">
|
||||
<button ref="btnClose" type="button" className="btn btn-default" onClick={this.closeModal}>{I18n.t('Close')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
});
|
||||
});
|
|
@ -6,9 +6,10 @@ define([
|
|||
'react',
|
||||
'jsx/external_apps/components/EditExternalToolButton',
|
||||
'jsx/external_apps/components/DeleteExternalToolButton',
|
||||
'jsx/external_apps/components/ConfigureExternalToolButton',
|
||||
'jsx/external_apps/lib/classMunger',
|
||||
'jquery.instructure_misc_helpers'
|
||||
], function(_, I18n, React, EditExternalToolButton, DeleteExternalToolButton, classMunger) {
|
||||
], function(_, I18n, React, EditExternalToolButton, DeleteExternalToolButton, ConfigureExternalToolButton, classMunger) {
|
||||
|
||||
return React.createClass({
|
||||
displayName: 'ExternalToolsTableRow',
|
||||
|
@ -19,8 +20,13 @@ define([
|
|||
|
||||
renderButtons() {
|
||||
if (this.props.tool.installed_locally) {
|
||||
var configureButton = null;
|
||||
if (this.props.tool.tool_configuration) {
|
||||
configureButton = <ConfigureExternalToolButton ref="configureExternalToolButton" tool={this.props.tool} />;
|
||||
}
|
||||
return (
|
||||
<td className="links text-right" nowrap="nowrap">
|
||||
{configureButton}
|
||||
<EditExternalToolButton ref="editExternalToolButton" tool={this.props.tool} />
|
||||
<DeleteExternalToolButton ref="deleteExternalToolButton" tool={this.props.tool} />
|
||||
</td>
|
||||
|
|
|
@ -49,7 +49,7 @@ define([
|
|||
};
|
||||
|
||||
store.fetch = function () {
|
||||
var url = this.getState().links.next || ENV.CONTEXT_BASE_URL + '/app_center/apps?per_page=' + PER_PAGE;
|
||||
var url = this.getState().links.next || '/api/v1' + ENV.CONTEXT_BASE_URL + '/app_center/apps?per_page=' + PER_PAGE;
|
||||
this.setState({ isLoading: true });
|
||||
$.ajax({
|
||||
url: url,
|
||||
|
|
|
@ -44,7 +44,7 @@ define([
|
|||
};
|
||||
|
||||
store.fetch = function() {
|
||||
var url = this.getState().links.next || ENV.CONTEXT_BASE_URL + '/lti_apps?per_page=' + PER_PAGE;
|
||||
var url = this.getState().links.next || '/api/v1' + ENV.CONTEXT_BASE_URL + '/lti_apps?per_page=' + PER_PAGE;
|
||||
this.setState({ isLoading: true });
|
||||
$.ajax({
|
||||
url: url,
|
||||
|
@ -56,10 +56,10 @@ define([
|
|||
|
||||
store.fetchWithDetails = function(tool) {
|
||||
if (tool.app_type === 'ContextExternalTool') {
|
||||
return $.getJSON(ENV.CONTEXT_BASE_URL + '/external_tools/' + tool.app_id);
|
||||
return $.getJSON('/api/v1' + ENV.CONTEXT_BASE_URL + '/external_tools/' + tool.app_id);
|
||||
} else {
|
||||
// DOES NOT EXIST YET
|
||||
return $.getJSON(ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id);
|
||||
return $.getJSON('/api/v1' + ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -68,10 +68,10 @@ define([
|
|||
|
||||
var params = this._generateParams(configurationType, data);
|
||||
|
||||
var url = ENV.CONTEXT_BASE_URL + '/external_tools';
|
||||
var url = '/api/v1' + ENV.CONTEXT_BASE_URL + '/external_tools';
|
||||
var method = 'POST';
|
||||
if (data.app_id) {
|
||||
url = ENV.CONTEXT_BASE_URL + '/external_tools/' + data.app_id;
|
||||
url = '/api/v1' + ENV.CONTEXT_BASE_URL + '/external_tools/' + data.app_id;
|
||||
method = 'PUT';
|
||||
}
|
||||
$.ajax({
|
||||
|
@ -87,9 +87,9 @@ define([
|
|||
var url;
|
||||
|
||||
if (tool.app_type === 'ContextExternalTool') {
|
||||
url = ENV.CONTEXT_BASE_URL + '/external_tools/' + tool.app_id;
|
||||
url = '/api/v1' + ENV.CONTEXT_BASE_URL + '/external_tools/' + tool.app_id;
|
||||
} else { // Lti::ToolProxy
|
||||
url = ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id;
|
||||
url = '/api/v1' + ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id;
|
||||
}
|
||||
|
||||
var tools = _.filter(this.getState().externalTools, function(t) { return t.app_id !== tool.app_id; });
|
||||
|
@ -104,7 +104,7 @@ define([
|
|||
};
|
||||
|
||||
store.activate = function(tool, success, error) {
|
||||
var url = ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id;
|
||||
var url = '/api/v1' + ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id;
|
||||
var tools = _.map(this.getState().externalTools, function(t) {
|
||||
if (t.app_id === tool.app_id) {
|
||||
t['enabled'] = true;
|
||||
|
@ -123,7 +123,7 @@ define([
|
|||
};
|
||||
|
||||
store.deactivate = function(tool, success, error) {
|
||||
var url = ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id;
|
||||
var url = '/api/v1' + ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id;
|
||||
var tools = _.map(this.getState().externalTools, function(t) {
|
||||
if (t.app_id === tool.app_id) {
|
||||
t['enabled'] = false;
|
||||
|
@ -133,7 +133,7 @@ define([
|
|||
this.setState({ externalTools: sort(tools) });
|
||||
|
||||
$.ajax({
|
||||
url: ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id,
|
||||
url: '/api/v1' + ENV.CONTEXT_BASE_URL + '/tool_proxies/' + tool.app_id,
|
||||
data: { workflow_state: 'disabled' },
|
||||
type: 'PUT',
|
||||
success: success.bind(this),
|
||||
|
|
|
@ -46,7 +46,8 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
:user_navigation, :course_navigation, :account_navigation, :resource_selection,
|
||||
:editor_button, :homework_submission, :migration_selection, :course_home_sub_navigation,
|
||||
:course_settings_sub_navigation, :global_navigation,
|
||||
:assignment_menu, :file_menu, :discussion_topic_menu, :module_menu, :quiz_menu, :wiki_page_menu
|
||||
:assignment_menu, :file_menu, :discussion_topic_menu, :module_menu, :quiz_menu, :wiki_page_menu,
|
||||
:tool_configuration
|
||||
]
|
||||
|
||||
CUSTOM_EXTENSION_KEYS = {:file_menu => [:accept_media_types]}
|
||||
|
|
|
@ -158,6 +158,9 @@ See `app/jsx/examples/ReactModalExample.jsx` for complete example usage.
|
|||
&.ReactModal__Body--force-no-corners {
|
||||
border-top-left-radius: 0; border-top-right-radius: 0;
|
||||
}
|
||||
&.ReactModal__Body--force-no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ReactModal__Footer {
|
||||
|
@ -186,4 +189,4 @@ See `app/jsx/examples/ReactModalExample.jsx` for complete example usage.
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -518,9 +518,11 @@ CanvasRails::Application.routes.draw do
|
|||
resources :external_tools do
|
||||
get :finished
|
||||
get :resource_selection
|
||||
collection do
|
||||
get :retrieve
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
get 'lti/basic_lti_launch_request/:message_handler_id', controller: 'lti/message', action: 'basic_lti_launch_request', as: :basic_lti_launch_request
|
||||
get 'lti/tool_proxy_registration', controller: 'lti/message', action: 'registration', as: :tool_proxy_registration
|
||||
get 'lti/registration_return', controller: 'lti/message', action: 'registration_return', as: :registration_return
|
||||
|
|
|
@ -52,7 +52,8 @@ module Lti
|
|||
name: external_tool.name,
|
||||
description: external_tool.description,
|
||||
installed_locally: external_tool.context == @context,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
tool_configuration: external_tool.tool_configuration
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -63,7 +64,8 @@ module Lti
|
|||
name: tool_proxy.name,
|
||||
description: tool_proxy.description,
|
||||
installed_locally: tool_proxy.context == @context,
|
||||
enabled: tool_proxy.workflow_state == 'active'
|
||||
enabled: tool_proxy.workflow_state == 'active',
|
||||
tool_configuration: nil
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -480,6 +480,7 @@ describe ExternalToolsController, type: :request do
|
|||
"consumer_key"=>"oi",
|
||||
"domain"=>nil,
|
||||
"url"=>"http://www.example.com/ims/lti",
|
||||
"tool_configuration"=>nil,
|
||||
"id"=>et ? et.id : nil,
|
||||
"not_selectable"=> et ? et.not_selectable : nil,
|
||||
"workflow_state"=>"public",
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
define [
|
||||
'react'
|
||||
'react-modal'
|
||||
'jsx/external_apps/components/ConfigureExternalToolButton'
|
||||
], (React, Modal, ConfigureExternalToolButton) ->
|
||||
|
||||
TestUtils = React.addons.TestUtils
|
||||
Simulate = TestUtils.Simulate
|
||||
wrapper = document.getElementById('fixtures')
|
||||
|
||||
Modal.setAppElement(wrapper)
|
||||
|
||||
createElement = (tool) ->
|
||||
ConfigureExternalToolButton({
|
||||
tool: tool
|
||||
})
|
||||
|
||||
renderComponent = (data) ->
|
||||
React.renderComponent(createElement(data), wrapper)
|
||||
|
||||
getDOMNodes = (data) ->
|
||||
component = renderComponent(data)
|
||||
btnTriggerModal = component.refs.btnTriggerModal?.getDOMNode()
|
||||
[component, btnTriggerModal]
|
||||
|
||||
module 'ExternalApps.ConfigureExternalToolButton',
|
||||
setup: ->
|
||||
@tools = [
|
||||
{
|
||||
"app_id": 1,
|
||||
"app_type": "ContextExternalTool",
|
||||
"description": "Talent provides an online, interactive video platform for professional development",
|
||||
"enabled": true,
|
||||
"installed_locally": true,
|
||||
"name": "Talent",
|
||||
"tool_configuration": { "url": "http://example.com" }
|
||||
},
|
||||
{
|
||||
"app_id": 2,
|
||||
"app_type": "Lti::ToolProxy",
|
||||
"description": null,
|
||||
"enabled": true,
|
||||
"installed_locally": true,
|
||||
"name": "Twitter",
|
||||
"tool_configuration": null
|
||||
},
|
||||
{
|
||||
"app_id": 3,
|
||||
"app_type": "Lti::ToolProxy",
|
||||
"description": null,
|
||||
"enabled": false,
|
||||
"installed_locally": true,
|
||||
"name": "Facebook",
|
||||
"tool_configuration": null
|
||||
}
|
||||
]
|
||||
teardown: ->
|
||||
React.unmountComponentAtNode wrapper
|
||||
|
||||
test 'open and close modal', ->
|
||||
tool = {
|
||||
"app_id": 1
|
||||
"app_type": "ContextExternalTool"
|
||||
"description": "Talent provides an online, interactive video platform for professional development"
|
||||
"enabled": true
|
||||
"installed_locally": true
|
||||
"name": "Talent"
|
||||
"tool_configuration": { "url": "http://example.com" }
|
||||
}
|
||||
[component, btnTriggerModal] = getDOMNodes(tool)
|
||||
Simulate.click(btnTriggerModal)
|
||||
ok component.state.modalIsOpen, 'modal is open'
|
||||
ok component.refs.btnClose
|
||||
Simulate.click(component.refs.btnClose.getDOMNode())
|
||||
ok !component.state.modalIsOpen, 'modal is not open'
|
||||
ok !component.refs.btnClose
|
|
@ -59,7 +59,8 @@ module Lti
|
|||
name: tool_proxy.name,
|
||||
description: tool_proxy.description,
|
||||
installed_locally: true,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
tool_configuration: nil
|
||||
})
|
||||
|
||||
end
|
||||
|
@ -77,7 +78,8 @@ module Lti
|
|||
name: external_tool.name,
|
||||
description: external_tool.description,
|
||||
installed_locally: true,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
tool_configuration: nil
|
||||
})
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue