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:
Eric Berry 2015-01-21 09:37:45 -07:00
parent 3d5c10e55f
commit ca2187815e
13 changed files with 209 additions and 21 deletions

View File

@ -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

View File

@ -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)

View File

@ -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>
)
}
});
});

View File

@ -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>

View File

@ -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,

View File

@ -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),

View File

@ -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]}

View File

@ -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.
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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