allow submission type LTIs to require a resource

Adds an option `require_resource_selection` for the
`submission_type_selection` placement to force a user to select a
resource (by launching the tool) before saving the assignment.

closes INTEROP-8626
flag=none

Note that the validation error message is intentionally brief to avoid
overflowing the allotted size for the error message popup/tooltip.

Test plan:
- have a tool with the submission_type_selection placement. In
  accordance with ContextExternalTool#placement_allowed?, you may need
  to add the domain or dev key to the allowed keys or allowed domains
  setting.
- edit the tool config (dev key config if the tool is 1.3) to have
  "require_resource_selection": true in the submission_type_selection
  placement settings.
- start creating a new assignment. Test out lots of different scenarios,
  but attempting to save the tool (for just testing whether the new
  validation triggers or not without actually saving the tool, you can
  always set the assignment title to be empty so the form won't actually
  submit), such as:
  - choosing submission type = "My Tool" and not doing anything else.
    there should be a validation error "Please click below to launch the
    tool and select a resource."
  - choosing submission type = "My Tool" and then launching and
    selecting a resource (no validation error)
  - selecting a resource and then hitting the 'x' to remove it
    (validation error)
  - selecting a resource, hitting the 'x' to remove it, choosing 'No
    Submission' and choosing 'My Tool' again. there shouldn't be any
    resource shown, and there should be a validation error.
  - selecting a resource, hitting the 'x' to remove it, clicking the
    button to launch the tool but closing the window before choosing
    another ressource. IT should show no resource and the validation
    error should trigger. Before this commit, it was showing the old (removed)
    resource.
  - selecting a resource, then choosing "External Tool". the URL should
    field should be emptied out and saving should fail with the
    (already existing) 'no URL' validation error.
  - selecting a resource, then choosing "External URL", and entering in
    some URL. this should succeed (make sure neither the "no URL" error
    or the new validation error are triggered)
  - choosing "External Tool" in the dropdown, then launching the tool in
    the normal assignment_selection placement and selecting a resource
    there. saving should succeed (this commit should only affect
    the submission_type_selection placement)
  - selecting a resource through assignment_selection as in the last
    step, and then choosing the tool in the dropdown
    (submission_type_selection placement). It should not show a resource
    and the validation should fail.
  - when you are done testing, choose the tool (as submission type
    selection) with content and save the assignment. ideally, send some
    custom variables in the deep linking response so you can test that
    those are maintained when editing the assignment.
- edit the assignment and test a variety of scenarios, including:
  - changing only title or description (don't type submission type at
    all). Save the tool. Check the Lti::ResourceLink in the database to
    make sure that the custom URL and/or custom parameters there have
    been preserved
  - switching to "No Submission" and back to the tool. This should
    show the resource preserved. Save and make sure the
    Lti::ResourceLink has been preserved.
  - switching to "External Tool". this should empty out the URL field
    and make the validation fail.
  - switching to "External Tool" as in the last check, but then set the
    dropdown back to the tool. There should not be a resource shown and
    saving should fire the validation.
  - edit again (reload the page if necessary) and just clear out the
    resource by clicking the 'x' under in the resource card. validation
    should fail.
  - remove and add back the resource. that should succeed (database
    Lti::ResourceLink should be updated with new data)
  - choose "External Tool" and choose the tool in assignment_selection.
    choose a resource and save the assignment. That should succeed and
    the Lti::ResourceLink in the DB should be updated.
- make an assignment with some other (ideally LTI 1.3) tool that doesn't
  have the submission_type_selection placement (or temporarily
  remove/disable submission_type_placement on your tool). edit the
  assignment without changing anything regarding submission type to make
  sure that still doesn't modify the resource link (if LTI 1.3)
- remove the require_resource_selection option on the tool
  and make sure you can create a new assignment using that tool's
  submission_type_selection placement without selecting a resource.
- if possible, to make sure we didn't severely make anything with
  MasteryConnect, test with a tool that acts like mastery connect:
  - change this line in the EditView.jsx:
    const mc_ext = item[LTI_EXT_MASTERY_CONNECT]
    to:
      const mc_ext = {points: 1, title: "foo", objectives: [],
        trackerName: 'foo', trackerAlignment: 'foo', studentCount: 0}
  - add an LTI 1.1 (cannot be 1.3) tool (such as Xander's vercel app)
    with the submission_type_selection placement and use
      Setting.set("submission_type_selection_allowed_launch_domains",
      ...) to allow it to use the placement (or stub out
      allowed_placement?)
  - play around with editing and editing a tool in the submission type
    selection placement. You will probably notice some difference (URL
    isn't cleared when you choose 'External Tool', values still exist
    when you choose the tool in the dropdown) but nothing should be too
  broken. At the very least, make sure you create an assignment with the
  tool, and then can edit the assignment and edit the description/title
  without changing the LTI stuff.

Change-Id: I86966abe815f6370db9e5723802398491c6fb66d
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/348871
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Bence Árpási <bence.arpasi@instructure.com>
QA-Review: Bence Árpási <bence.arpasi@instructure.com>
Product-Review: Alexis Nast <alexis.nast@instructure.com>
This commit is contained in:
Evan Battaglia 2024-05-31 22:41:21 +00:00
parent c5b024ad71
commit fed8e96adc
10 changed files with 385 additions and 147 deletions

View File

@ -548,27 +548,32 @@ class ApplicationController < ActionController::Base
helper_method :external_tools_display_hashes
def external_tool_display_hash(tool, type, url_params = {}, context = @context, custom_settings = [])
url_params = {
id: tool.id,
launch_type: type
}.merge(url_params)
hash = {
id: tool.id,
title: tool.label_for(type, I18n.locale),
base_url: polymorphic_url([context, :external_tool], url_params),
}
hash[:tool_id] = tool.tool_id if tool.tool_id.present?
base_url: polymorphic_url(
[context, :external_tool],
{ id: tool.id, launch_type: type }.merge(url_params)
),
tool_id: tool.tool_id.presence
}.compact
extension_settings = [:icon_url, :canvas_icon_class] | custom_settings
extension_settings.each do |setting|
hash[setting] = tool.extension_setting(type, setting)
end
hash[:base_title] = tool.default_label(I18n.locale) if custom_settings.include?(:base_title)
hash[:external_url] = tool.url if custom_settings.include?(:external_url)
if type == :submission_type_selection && tool.submission_type_selection[:description].present?
hash[:description] = tool.submission_type_selection[:description]
if type == :submission_type_selection
hash.merge!({
description: tool.submission_type_selection[:description].presence,
require_resource_selection:
tool.submission_type_selection[:require_resource_selection]
}.compact)
end
if type == :top_navigation
hash[:pinned] = tool.placement_pinned?(type)
end

View File

@ -105,7 +105,8 @@ class ContextExternalTool < ActiveRecord::Base
CUSTOM_EXTENSION_KEYS = {
file_menu: [:accept_media_types].freeze,
editor_button: [:use_tray].freeze
editor_button: [:use_tray].freeze,
submission_type_selection: [:description, :require_resource_selection].freeze,
}.freeze
DISABLED_STATE = "disabled"

View File

@ -139,6 +139,9 @@ module Schemas::Lti
"maxLength" => 255,
"errorMessage" => "description must be a string with a maximum length of 255 characters"
}.freeze,
"require_resource_selection" => {
"type" => "boolean"
}.freeze,
**LAUNCH_INFO_SCHEMA,
}.freeze
}.freeze

View File

@ -2273,22 +2273,28 @@ describe AssignmentsController do
let(:domain) { "justanexamplenotarealwebsite.com" }
let(:tool) do
factory_with_protected_attributes(@course.context_external_tools,
domain:,
url: "http://www.justanexamplenotarealwebsite.com/tool1",
shared_secret: "test123",
consumer_key: "test123",
name: tool_settings[:base_title],
settings: {
submission_type_selection: tool_settings
})
factory_with_protected_attributes(
@course.context_external_tools,
domain:,
url: "http://www.justanexamplenotarealwebsite.com/tool1",
shared_secret: "test123",
consumer_key: "test123",
name: tool_settings[:base_title],
settings: {
submission_type_selection: tool_settings
}
)
end
let(:tool_in_js_env) do
Setting.set("submission_type_selection_allowed_launch_domains", domain)
tool
subject
assigns[:js_env][:SUBMISSION_TYPE_SELECTION_TOOLS][0]
end
it "is correctly set" do
tool
Setting.set("submission_type_selection_allowed_launch_domains", domain)
subject
expect(assigns[:js_env][:SUBMISSION_TYPE_SELECTION_TOOLS][0]).to include(
expect(tool_in_js_env).to include(
base_title: tool_settings[:base_title],
title: tool_settings[:base_title],
selection_width: tool_settings[:selection_width],
@ -2296,20 +2302,36 @@ describe AssignmentsController do
)
end
context "the tool includes a description propery" do
let(:description) { "This is a description" }
let(:tool_settings) do
res = super()
res[:description] = description
res
describe "require_resourse_selection property" do
context "when not given in the settings" do
it "is not set in the js_env tool" do
expect(tool_in_js_env).to_not include(:require_resource_selection)
end
end
context "when set if set to false in the settings" do
let(:tool_settings) { super().merge(require_resource_selection: false) }
it "is set in the js_env tool" do
expect(tool_in_js_env).to include(require_resource_selection: false)
end
end
context "when set if set to true in the settings" do
let(:tool_settings) { super().merge(require_resource_selection: true) }
it "is set in the js_env tool" do
expect(tool_in_js_env).to include(require_resource_selection: true)
end
end
end
context "the tool includes a description propery" do
let(:description) { "This is a description" }
let(:tool_settings) { super().merge(description:) }
it "includes the launch points" do
tool
Setting.set("submission_type_selection_allowed_launch_domains", domain)
subject
expect(assigns[:js_env][:SUBMISSION_TYPE_SELECTION_TOOLS][0])
.to include(description:)
expect(tool_in_js_env).to include(description:)
end
end
end

View File

@ -1215,6 +1215,19 @@ describe ContextExternalTool do
expect(@tool.extension_setting(:file_menu, :accept_media_types)).to eq "types"
end
it "allows description and require_resource_selection exclusively for submission_type_selection extension" do
@tool = external_tool_model
description = "my description"
require_resource_selection = true
@tool.submission_type_selection = { description:, require_resource_selection: }
@tool.file_menu = { description:, require_resource_selection: }
@tool.save!
expect(@tool.extension_setting(:submission_type_selection, :description)).to eq description
expect(@tool.extension_setting(:submission_type_selection, :require_resource_selection)).to eq require_resource_selection
expect(@tool.extension_setting(:file_menu, :description)).to be_blank
expect(@tool.extension_setting(:file_menu, :require_resource_selection)).to be_blank
end
it "clears disabled extensions" do
@tool = @course.context_external_tools.create!(name: "a", url: "http://google.com", consumer_key: "12345", shared_secret: "secret")
@tool.course_navigation = {

View File

@ -37,6 +37,17 @@ module Lti
let(:tool_configuration) { described_class.new(settings:) }
let(:developer_key) { DeveloperKey.create }
def make_placement(type, message_type, extra = {})
{
"target_link_uri" => "http://example.com/launch?placement=#{type}",
"text" => "Test Title",
"message_type" => message_type,
"icon_url" => "https://static.thenounproject.com/png/131630-211.png",
"placement" => type.to_s,
**extra
}
end
describe "validations" do
subject { tool_configuration.save }
@ -50,20 +61,27 @@ module Lti
context "with a description property at the submission_type_selection placement" do
let(:settings) do
res = super()
super().tap do |res|
res["extensions"].first["settings"]["placements"] << make_placement(
:submission_type_selection,
"LtiDeepLinkingRequest",
"description" => "Test Description"
)
end
end
res["extensions"].first["settings"]["placements"].push(
{
"target_link_uri" => "http://example.com/launch?placement=submission_type_selection",
"text" => "Test Title",
"message_type" => "LtiResourceLinkRequest",
"icon_url" => "https://static.thenounproject.com/png/131630-211.png",
"description" => "Test Description",
"placement" => "submission_type_selection",
}
)
it { is_expected.to be true }
end
res
context "with a require_resource_selection property at the submission_type_selection placement" do
let(:settings) do
super().tap do |res|
res["extensions"].first["settings"]["placements"] << make_placement(
:submission_type_selection,
"LtiDeepLinkingRequest",
"require_resource_selection" => true
)
end
end
it { is_expected.to be true }
@ -92,20 +110,27 @@ module Lti
context "when the submission_type_selection description is longer than 255 characters" do
let(:settings) do
s = super()
super().tap do |s|
s["extensions"].first["settings"]["placements"] << make_placement(
:submission_type_selection,
"LtiDeepLinkingRequest",
"description" => "a" * 256
)
end
end
s["extensions"].first["settings"]["placements"].push(
{
"target_link_uri" => "http://example.com/launch?placement=submission_type_selection",
"text" => "Test Title",
"message_type" => "LtiResourceLinkRequest",
"icon_url" => "https://static.thenounproject.com/png/131630-211.png",
"description" => "a" * 256,
"placement" => "submission_type_selection",
}
)
it { is_expected.to be false }
end
s
context "when the submission_type_selection require_resource_selection is of the wrong type" do
let(:settings) do
super().tap do |s|
s["extensions"].first["settings"]["placements"] << make_placement(
:submission_type_selection,
"LtiDeepLinkingRequest",
"require_resource_selection" => "true"
)
end
end
it { is_expected.to be false }
@ -702,15 +727,10 @@ module Lti
context "when the configuration has a #{placement} placement" do
let(:tool_configuration) do
tc = super()
tc.settings["extensions"].first["settings"]["placements"] << {
"placement" => placement,
"message_type" => "LtiResourceLinkRequest",
"target_link_uri" => "http://example.com/launch?placement=#{placement}"
}
tc
super().tap do |tc|
tc.settings["extensions"].first["settings"]["placements"] <<
make_placement(placement, "LtiResourceLinkRequest")
end
end
it { is_expected.to include("Warning").and include(placement) }

View File

@ -679,20 +679,22 @@ EditView.prototype.handleContentItem = function (item) {
}
}
this.renderAssignmentSubmissionTypeContainer({
tool: this.selectedTool || {title: item.title},
resource: item,
})
this.renderAssignmentSubmissionTypeContainer()
// TODO: add date prefill here
}
EditView.prototype.handleRemoveResource = function () {
// Restore things to how they were before the user pushed the button to
// launch the submission_type_selection tool
this.$externalToolsContentType.val('context_external_tool')
this.$externalToolsUrl.val(this.selectedTool.external_url)
this.$externalToolsTitle.val('')
this.$externalToolsCustomParams.val('')
this.$externalToolsIframeWidth.val('')
this.$externalToolsIframeHeight.val('')
this.renderAssignmentSubmissionTypeContainer()
}
/**
@ -954,14 +956,30 @@ EditView.prototype.handleGradingTypeChange = function (gradingType) {
return this.handleSubmissionTypeChange(null)
}
EditView.prototype.hasMasteryConnectData = function () {
// Some places check for this data before clearing/overwriting...
// It's not clear the reasoning behind this, but I'm for leaving as-is for now.
// If some of the resulting odd behavior (switching to MasteryCoinnect from another submission type placement tool) surfaces, revisit with Mastery Connect team (git ref ef3249f62f)
return !!this.$externalToolExternalData.val()
}
EditView.prototype.handleSubmissionTypeChange = function (_ev) {
const subVal = this.$submissionType.val()
this.$onlineSubmissionTypes.toggleAccessibly(subVal === 'online')
this.$externalToolSettings.toggleAccessibly(subVal === 'external_tool')
const isPlacementTool = subVal.includes('external_tool_placement')
this.$externalToolPlacementLaunchContainer.toggleAccessibly(isPlacementTool)
if (isPlacementTool) {
this.handlePlacementExternalToolSelect(subVal)
} else if (this.selectedTool && subVal === 'external_tool' && !this.hasMasteryConnectData()) {
// Moving from a submission_type_placement tool to generic
// "External Tool" type: empty out the fields. this prevents people
// from working around the "require_resource_selection" field just
// by choosing the tool and then choosing "External Tool"
this.handleRemoveResource()
this.$externalToolsUrl.val('')
this.selectedTool = undefined
}
this.$groupCategorySelector.toggleAccessibly(subVal !== 'external_tool' && !isPlacementTool)
this.$peerReviewsFields.toggleAccessibly(subVal !== 'external_tool' && !isPlacementTool)
@ -1012,34 +1030,34 @@ EditView.prototype.handlePlacementExternalToolSelect = function (selection) {
return toolId === tool.id
})
const hasNoExtToolData = !this.$externalToolExternalData.val()
const toolIdsMatch = toolId === this.assignment.selectedSubmissionTypeToolId()
const extToolUrlsMatch = this.$externalToolsUrl.val() === this.assignment.externalToolUrl()
if (hasNoExtToolData && !(toolIdsMatch && extToolUrlsMatch)) {
// Set the URL of the tool, but only if we haven't just come back from a
// deep linking response (so we'll have a URL from the deep linking
// response); also don't set if we are just editing the assignment (in this
// case the URL [this.$externalToolsUrl] will be the assignment URL)
if (!this.hasMasteryConnectData() && !(toolIdsMatch && extToolUrlsMatch)) {
// When switching to a submission_type_selection tool in the dropdown, set
// the URL of the tool. Don't set if we are just editing the assignment
// (toolIdsMatch and extToolUrlsMatch).
this.$externalToolsUrl.val(this.selectedTool.external_url)
// Ensure that custom params & other stuff left over from another previous
// deep link response get cleared out when the user chooses a tool:
this.$externalToolsCustomParams.val('')
this.$externalToolsIframeWidth.val('')
this.$externalToolsIframeHeight.val('')
this.$externalToolsTitle.val('')
} else if (this.assignment.resourceLink() && this.assignment.resourceLink().title) {
// NOTE: not sure this is necessary, but not risking changing now.
this.$externalToolsTitle.val(this.assignment.resourceLink().title)
}
this.renderAssignmentSubmissionTypeContainer({
tool: this.selectedTool,
resource: this.assignment.resourceLink(),
})
this.renderAssignmentSubmissionTypeContainer()
}
EditView.prototype.renderAssignmentSubmissionTypeContainer = function ({tool, resource}) {
EditView.prototype.renderAssignmentSubmissionTypeContainer = function () {
const resource = {title: this.$externalToolsTitle.val()}
const props = {
tool,
tool: this.selectedTool,
resource,
onRemoveResource: this.handleRemoveResource,
onLaunchButtonClick: this.handleSubmissionTypeSelectionLaunch,
@ -1289,6 +1307,7 @@ EditView.prototype._adjustDateValue = function (newDate, originalDate) {
EditView.prototype.getFormData = function () {
let data
data = EditView.__super__.getFormData.apply(this, arguments)
data.submission_type = this.toolSubmissionType(data.submission_type)
data = this._inferSubmissionTypes(data)
data = this._filterAllowedExtensions(data)
data = this._unsetGroupsIfExternalTool(data)
@ -1364,7 +1383,6 @@ EditView.prototype.submit = function (event) {
event.preventDefault()
event.stopPropagation()
this.cacheAssignmentSettings()
$(SUBMISSION_TYPE).val(this.toolSubmissionType($(SUBMISSION_TYPE).val()))
if (this.dueDateOverrideView.containsSectionsWithoutOverrides()) {
sections = this.dueDateOverrideView.sectionsWithoutOverrides()
missingDateDialog = new MissingDateDialog({
@ -1704,9 +1722,12 @@ EditView.prototype._validatePointsRequired = function (data, errors) {
}
EditView.prototype._validateExternalTool = function (data, errors) {
let message, ref, ref1
if (data.submission_type !== 'external_tool') {
return errors
}
let ref, ref1
if (
data.submission_type === 'external_tool' &&
data.grading_type !== 'not_graded' &&
$.trim(
(ref = data.external_tool_tag_attributes) != null
@ -1716,18 +1737,27 @@ EditView.prototype._validateExternalTool = function (data, errors) {
: void 0
).length === 0
) {
message = I18n.t('External Tool URL cannot be left blank')
errors['external_tool_tag_attributes[url]'] = [
{
message,
},
]
errors['default-tool-launch-button'] = [
{
message,
},
]
const message = I18n.t('External Tool URL cannot be left blank')
errors['external_tool_tag_attributes[url]'] = [{message}]
errors['default-tool-launch-button'] = [{message}]
}
// This can happen when:
// * user chooses a tool in the submission type dropdown (Submission Type
// Selection placement) but doesn't launch the tool and finish deep linking
// flow
// * user edits assignment and removes resource by clicking the 'x'
// (we reset content_type back to 'context_external_tool' then)
if (
typeof data.external_tool_tag_attributes === 'object' &&
!data.external_tool_tag_attributes.title &&
this.selectedTool &&
this.selectedTool.require_resource_selection
) {
const message = I18n.t('Please click below to launch the tool and select a resource.')
errors.assignment_submission_container = [{message}]
}
return errors
}

View File

@ -37,6 +37,7 @@ import userSettings from '@canvas/user-settings'
import assertions from '@canvas/test-utils/assertionsSpec'
import '@canvas/jquery/jquery.simulate'
import ExternalToolModalLauncher from '@canvas/external-tools/react/components/ExternalToolModalLauncher'
import {AssignmentSubmissionTypeContainer} from '../../../react/AssignmentSubmissionTypeContainer'
const s_params = 'some super secure params'
const fixtures = document.getElementById('fixtures')
@ -154,6 +155,7 @@ QUnit.module('EditView', {
$('ul[id^=ui-id-]').remove()
$('.form-dialog').remove()
document.getElementById('fixtures').innerHTML = ''
sinon.restore()
},
editView() {
return editView.apply(this, arguments)
@ -365,6 +367,13 @@ test('does not allow point value of "" if grading type is letter', function () {
equal(view.getFormData().groupCategoryId, null)
})
test('does not allow blank default external tool url', function () {
const view = this.editView()
const data = {submission_type: 'external_tool'}
const errors = view._validateExternalTool(data, [])
equal(errors['default-tool-launch-button'][0].message, 'External Tool URL cannot be left blank')
})
test('does not allow blank external tool url', function () {
const view = this.editView()
const data = {submission_type: 'external_tool'}
@ -375,11 +384,165 @@ test('does not allow blank external tool url', function () {
)
})
test('does not allow blank default external tool url', function () {
// #region "tests regarding Submission Type Selection"
test('does not allow submission_type_selection tools (selectedTool set) with require_resource_selection=true with no resource title', function () {
const view = this.editView()
const data = {submission_type: 'external_tool'}
view.selectedTool = {require_resource_selection: true}
const data = {
submission_type: 'external_tool',
external_tool_tag_attributes: {
content_type: 'context_external_tool',
},
}
const errors = view._validateExternalTool(data, [])
equal(errors['default-tool-launch-button'][0].message, 'External Tool URL cannot be left blank')
deepEqual(
errors.assignment_submission_container[0].message,
'Please click below to launch the tool and select a resource.'
)
})
test('allows submission_type_selection tools (selectedTool set) with require_resource_selection not set with no resource title', function () {
const view = this.editView()
view.selectedTool = {}
const data = {
submission_type: 'external_tool',
external_tool_tag_attributes: {content_type: 'context_external_tool'},
}
const errors = view._validateExternalTool(data, [])
notOk(errors.assignment_submission_container)
})
test('allows submission_type_selection tools with require_resource_selection=true with a resource title', function () {
const view = this.editView()
view.selectedTool = {require_resource_selection: true}
const data = {
submission_type: 'external_tool',
external_tool_tag_attributes: {
content_type: 'context_external_tool',
title: 'some title',
},
}
const errors = view._validateExternalTool(data, [])
notOk(errors.assignment_submission_container)
})
// sets up the editView and chooses a submission_type_selection tool
// calls block with the edit view
// resets mocks at the end
function editViewWithSubmissionTypeSelection() {
const tool = {
id: '123',
title: 'foo',
description: 'bar',
external_url: 'http://example.com',
require_resource_selection: true,
}
ENV.SUBMISSION_TYPE_SELECTION_TOOLS = [tool]
const view = editView()
const types = [...view.$submissionType.find('option')].map(el => el.value)
ok(types.includes('external_tool_placement_123'))
// ignore render in renderSubmissionTypeSelectionDialog:
sinon.mock(ReactDOM).expects('render').atLeast(0)
// tests will check what is rendered to the SubmissionTypeSelection card
sinon.spy(React, 'createElement')
return view
}
test('when submission_type_selection tool chosen: sets selectedTool and leaves title empty', function () {
const view = editViewWithSubmissionTypeSelection()
view.$submissionType.val('external_tool_placement_123')
view.$submissionType.trigger('change')
const formData = view.getFormData()
// Needed for proper creation of the assignment on the backend, and
// validation in _validateExternalTool()
equal(formData.submission_type, 'external_tool')
deepEqual(formData.submission_types, ['external_tool'])
ok(view.selectedTool.require_resource_selection)
notOk(formData.external_tool_tag_attributes.title)
})
const lastSubmissionTypeContainerProps = function () {
const calls = React.createElement
.getCalls()
.filter(call => call.args[0] === AssignmentSubmissionTypeContainer)
return calls[calls.length - 1].args[1]
}
test('when a submission_type_selection tool chosen and a resource selected: sets selectedTool and title', function () {
const view = editViewWithSubmissionTypeSelection()
view.$submissionType.val('external_tool_placement_123')
view.$submissionType.trigger('change')
view.handleContentItem({
type: 'ltiResourceLink',
custom: {},
url: 'http://example.com',
title: 'someResource',
lineItem: {},
})
const formData = view.getFormData()
ok(view.selectedTool.require_resource_selection)
equal(formData.external_tool_tag_attributes.title, 'someResource')
// Card is shown (props include title):
equal(lastSubmissionTypeContainerProps().resource.title, 'someResource')
})
test('when a submission_type_selection tool chosen, a resource selected, and the resource removed: keeps selectedTool but clears out title', function () {
const view = editViewWithSubmissionTypeSelection()
view.$submissionType.val('external_tool_placement_123')
view.$submissionType.trigger('change')
view.handleContentItem({
type: 'ltiResourceLink',
custom: {},
url: 'http://example.com',
title: 'someResourceLinkTitle',
lineItem: {},
})
view.handleRemoveResource()
const formData = view.getFormData()
ok(view.selectedTool.require_resource_selection)
notOk(formData.external_tool_tag_attributes.title)
// Card is NOT shown:
notOk(lastSubmissionTypeContainerProps().resource.title)
})
test('when a submission_type_select tool chosen but changed back to generic "External Tool": URL reset', function () {
// Moving from a submission_type_placement tool to generic
// clear out URL & selectedTool -- shouldn't carry over selectedTool
// and prefill external_tool URL
const view = editViewWithSubmissionTypeSelection()
view.$submissionType.val('external_tool_placement_123')
view.$submissionType.trigger('change')
view.handleContentItem({
type: 'ltiResourceLink',
custom: {},
url: 'http://example.com',
title: 'someResourceLinkTitle',
lineItem: {},
})
const formData = view.getFormData()
ok(view.selectedTool)
ok(formData.external_tool_tag_attributes.url)
view.$submissionType.val('external_tool')
view.$submissionType.trigger('change')
const newFormData = view.getFormData()
notOk(view.selectedTool)
notOk(newFormData.external_tool_tag_attributes.url)
})
test('does not validate allowed extensions if file uploads is not a submission type', function () {
@ -389,6 +552,8 @@ test('does not validate allowed extensions if file uploads is not a submission t
equal(errors.allowed_extensions, null)
})
// #endregion "tests regarding Submission Type Selection"
test('removes group_category_id if an external tool is selected', function () {
const view = this.editView()
let data = {

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {useState} from 'react'
import React from 'react'
import type {GlobalEnv} from '@canvas/global/env/GlobalEnv.d'
@ -24,7 +24,7 @@ import {AssignmentSubmissionTypeSelectionResourceLinkCard} from './AssignmentSub
import {AssignmentSubmissionTypeSelectionLaunchButton} from './AssignmentSubmissionTypeSelectionLaunchButton'
export type AssignmentSubmissionTypeContainerProps = {
tool: {
tool?: {
developer_key?: {
global_id: string
}
@ -44,43 +44,25 @@ declare const ENV: GlobalEnv & {
ASSIGNMENT_SUBMISSION_TYPE_CARD_ENABLED: boolean
}
export function AssignmentSubmissionTypeContainer(props: AssignmentSubmissionTypeContainerProps) {
const {resource, tool, onLaunchButtonClick, onRemoveResource} = props
const [removedResource, setRemovedResource] = useState(false)
return (
<>
{ENV.ASSIGNMENT_SUBMISSION_TYPE_CARD_ENABLED ? (
<>
{resource?.title && !removedResource ? (
<AssignmentSubmissionTypeSelectionResourceLinkCard
tool={tool}
resourceTitle={resource.title}
onCloseButton={() => {
setRemovedResource(true)
onRemoveResource()
}}
/>
) : (
<AssignmentSubmissionTypeSelectionLaunchButton
tool={tool}
onClick={() => {
setRemovedResource(false)
onLaunchButtonClick()
}}
/>
)}
</>
) : (
<AssignmentSubmissionTypeSelectionLaunchButton
tool={tool}
onClick={() => {
setRemovedResource(false)
onLaunchButtonClick()
}}
/>
)}
</>
)
export function AssignmentSubmissionTypeContainer(
props: AssignmentSubmissionTypeContainerProps
): React.ReactElement | null {
if (!props.tool) {
return null
} else if (ENV.ASSIGNMENT_SUBMISSION_TYPE_CARD_ENABLED && props.resource?.title) {
return (
<AssignmentSubmissionTypeSelectionResourceLinkCard
tool={props.tool}
resourceTitle={props.resource.title}
onCloseButton={props.onRemoveResource}
/>
)
} else {
return (
<AssignmentSubmissionTypeSelectionLaunchButton
tool={props.tool}
onClick={props.onLaunchButtonClick}
/>
)
}
}

View File

@ -65,7 +65,7 @@ describe('AssignmentSubmissionTypeContainer', () => {
expect(screen.queryByTestId('assignment_submission_type_selection_launch_button')).toBeFalsy()
})
it('renders the launch button again after user clicks the close button on the resource link card', () => {
it('calls onRemoveResource when the user clicks the resource close button', () => {
const resource = {title: 'Resource Title'}
renderComponent(resource)
expect(
@ -75,9 +75,6 @@ describe('AssignmentSubmissionTypeContainer', () => {
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('assignment_submission_type_selection_launch_button')).toBeTruthy()
expect(
screen.queryByTestId('assignment-submission-type-selection-resource-link-card')
).toBeFalsy()
expect(onRemoveResourceFn).toHaveBeenCalled()
})
})