From 40efea098779b1f9279a74e5fbc2506b721da00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 17 Apr 2026 10:08:57 -0300 Subject: [PATCH 01/21] feat: new editor design --- lti_consumer/lti_xblock.py | 213 +++-- .../static/css/xblock_studio_view.css | 331 ++++++++ lti_consumer/static/js/xblock_studio_view.js | 618 ++++++++++---- .../templates/html/lti_studio_edit.html | 755 ++++++++++++++++++ lti_consumer/tests/unit/test_lti_xblock.py | 12 +- 5 files changed, 1659 insertions(+), 270 deletions(-) create mode 100644 lti_consumer/static/css/xblock_studio_view.css create mode 100644 lti_consumer/templates/html/lti_studio_edit.html diff --git a/lti_consumer/lti_xblock.py b/lti_consumer/lti_xblock.py index 1adfbc34..1519fc29 100644 --- a/lti_consumer/lti_xblock.py +++ b/lti_consumer/lti_xblock.py @@ -144,14 +144,14 @@ def valid_config_type_values(block): valid value options, depending on the state of the appropriate toggle. """ values = [ - {"display_name": _("Configuration on block"), "value": "new"} + {"display_name": _("New"), "value": "new"} ] if database_config_enabled(block.scope_ids.usage_id.context_key): - values.append({"display_name": _("Database Configuration"), "value": "database"}) + values.append({"display_name": _("Database"), "value": "database"}) if external_config_filter_enabled(block.scope_ids.usage_id.context_key): - values.append({"display_name": _("Reusable Configuration"), "value": "external"}) + values.append({"display_name": _("Existing"), "value": "external"}) return values @@ -261,18 +261,15 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): display_name = String( display_name=_("Display Name"), help=_( - "Enter the name that students see for this component. " - "Analytics reports may also use the display name to identify this component." + "Enter the name learners see for this component. This name may also appear in reports." ), scope=Scope.settings, default=_("LTI Consumer"), ) description = String( - display_name=_("LTI Application Information"), + display_name=_("Data Sharing Notice"), help=_( - "Enter a description of the third party application. " - "If requesting username and/or email, use this text box to inform users " - "why their username and/or email will be forwarded to a third party application." + "Enter a short notice about the tool. Use this field to explain why learner data may be shared with it." ), default="", scope=Scope.settings @@ -283,9 +280,8 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): values_provider=valid_config_type_values, default="new", help=_( - "Select 'Configuration on block' to configure a new LTI Tool. " - "If the support staff provided you with a pre-configured LTI reusable Tool ID, select" - "'Reusable Configuration' and enter it in the text field below." + "Select 'New' to configure a new LTI Tool. If your admin has provided you with ID of an existing " + "reusable tool configuration, select 'Existing' and enter it in the text field on the right." ) ) @@ -298,16 +294,14 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): ], default="lti_1p1", help=_( - "Select the LTI version that your tool supports." - "
The XBlock LTI Consumer fully supports LTI 1.1.1, " - "LTI 1.3 and LTI Advantage features." + "Select the LTI version supported by your tool." ), ) external_config = String( - display_name=_("LTI Reusable Configuration ID"), + display_name=_("LTI Reusability ID"), scope=Scope.settings, - help=_("Enter the reusable LTI external configuration ID provided by the support staff."), + help=_("Enter the ID of an existing reusable tool configuration."), ) # LTI 1.3 fields @@ -319,32 +313,27 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): ) lti_1p3_launch_url = String( - display_name=_("Tool Launch URL"), + display_name=_("Launch URL"), default='', scope=Scope.settings, help=_( - "Enter the LTI 1.3 Tool Launch URL. " - "
This is the URL the LMS will use to launch the LTI Tool." + "Enter the launch URL for this tool. For LTI 1.3, this is the URL Open edX uses to launch the tool." ), ) lti_1p3_oidc_url = String( - display_name=_("Tool Initiate Login URL"), + display_name=_("Tool Initiate Login URL (OIDC)"), default='', scope=Scope.settings, help=_( - "Enter the LTI 1.3 Tool OIDC Authorization url (can also be called login or login initiation URL)." - "
This is the URL the LMS will use to start a LTI authorization " - "prior to doing the launch request." + "Enter the tool’s OIDC login initiation URL. Open edX uses this URL to start the LTI 1.3 login flow." ), ) lti_1p3_redirect_uris = List( display_name=_("Registered Redirect URIs"), help=_( - "Valid urls the Tool may request us to redirect the id token to. The redirect uris " - "are often the same as the launch url/deep linking url so if this field is " - "empty, it will use them as the default. If you need to use different redirect " - "uri's, enter them here. If you use this field you must enter all valid redirect " - "uri's the tool may request." + "Enter the redirect URIs this tool is allowed to use during login " + 'e.g. ["https://tool.com", "https://tool_deeplink.com"]. Leave this blank to use ' + "the launch URL and deep linking URL by default." ), scope=Scope.settings ) @@ -358,7 +347,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): ], default="public_key", help=_( - "Select how the tool's public key information will be specified." + "Choose how Open edX will get the tool’s public key for validating signed messages." ), ) lti_1p3_tool_keyset_url = String( @@ -366,16 +355,8 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): default='', scope=Scope.settings, help=_( - "Enter the LTI 1.3 Tool's JWK keysets URL." - "
This link should retrieve a JSON file containing" - " public keys and signature algorithm information, so" - " that the LMS can check if the messages and launch" - " requests received have the signature from the tool." - "
This is not required when doing LTI 1.3 Launches" - " without LTI Advantage nor Basic Outcomes requests." - "

Changing the public key or keyset URL will cause the client ID, block keyset URL " - "and access token URL to be regenerated if they are shared between blocks. " - "Please check and update them in the LTI tool settings if necessary." + "Enter the URL of the tool’s JWK keyset. Open edX uses this URL to retrieve the " + "public keys needed to validate signed messages." ), ) lti_1p3_tool_public_key = String( @@ -384,21 +365,17 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): default='', scope=Scope.settings, help=_( - "Enter the LTI 1.3 Tool's public key." - "
This is a string that starts with '-----BEGIN PUBLIC KEY-----' and is required " - "so that the LMS can check if the messages and launch requests received have the signature " - "from the tool." - "
This is not required when doing LTI 1.3 Launches without LTI Advantage nor " - "Basic Outcomes requests." - "

Changing the public key or keyset URL will cause the client ID, block keyset URL " - "and access token URL to be regenerated if they are shared between blocks. " - "Please check and update them in the LTI tool settings if necessary." + "Enter the tool’s public key in PEM format (starts with -----BEGIN PUBLIC KEY-----). " + "Use this when the tool provides a static public key directly." ), ) lti_1p3_enable_nrps = Boolean( - display_name=_("Enable LTI NRPS"), - help=_("Enable LTI Names and Role Provisioning Services."), + display_name=_("Names & Roles (NRPS)"), + help=_( + "Enable this to allow the tool to access the names and roles of enrolled learners, " + "if the tool supports NRPS." + ), default=False, scope=Scope.settings ) @@ -406,7 +383,9 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): # Switch to enable/disable the LTI Advantage Deep linking service lti_advantage_deep_linking_enabled = Boolean( display_name=_("Deep linking"), - help=_("Select True if you want to enable LTI Advantage Deep Linking."), + help=_( + "Enable this if you want to deep link content from within the tool, in Studio." + ), default=False, scope=Scope.settings ) @@ -415,34 +394,30 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): default='', scope=Scope.settings, help=_( - "Enter the LTI Advantage Deep Linking Launch URL. If the tool does not specify one, " - "use the same value as 'Tool Launch URL'." + "Enter the deep linking URL for this tool. If the tool does not specify one, enter the 'Launch URL' here." ), ) lti_advantage_ags_mode = String( - display_name=_("LTI Assignment and Grades Service"), + display_name=_("Assignment and Grades"), values=[ + {"display_name": _("Declarative"), "value": "declarative"}, + {"display_name": _("Programmatic"), "value": "programmatic"}, {"display_name": _("Disabled"), "value": "disabled"}, - {"display_name": _("Allow tools to submit grades only (declarative)"), "value": "declarative"}, - {"display_name": _("Allow tools to manage and submit grade (programmatic)"), "value": "programmatic"}, ], default='declarative', scope=Scope.settings, help=_( - "Enable the LTI-AGS service and select the functionality enabled for LTI tools. " - "The 'declarative' mode (default) will provide a tool with a LineItem created from the XBlock settings, " - "while the 'programmatic' one will allow tools to manage, create and link the grades." + "Enable AGS and select functionality. 'Declarative' provides the tool with an existing line item. " + "'Programmatic' allows tools to create and manage line items." ), ) # LTI 1.1 fields lti_id = String( - display_name=_("LTI ID"), + display_name=_("LTI Passport ID"), help=_( - "Enter the LTI ID for the external LTI provider. " - "This value must be the same LTI ID that you entered in the " - "LTI Passports setting on the Advanced Settings page." - "
See the {docs_anchor_open}edX LTI documentation{anchor_close} for more details on this setting." + "Select the LTI passport ID for this tool. This is the ID of the LTI passport that " + "you created on Advanced Settings page." ).format( docs_anchor_open=DOCS_ANCHOR_TAG_OPEN, anchor_close="" @@ -453,9 +428,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): launch_url = String( display_name=_("LTI URL"), help=_( - "Enter the URL of the external tool that this component launches. " - "This setting is only used when Hide External Tool is set to False." - "
See the {docs_anchor_open}edX LTI documentation{anchor_close} for more details on this setting." + "Enter the launch URL for this tool. For LTI 1.1/1.2, this is the URL Open edX uses to launch the tool." ).format( docs_anchor_open=DOCS_ANCHOR_TAG_OPEN, anchor_close="" @@ -468,9 +441,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): custom_parameters = List( display_name=_("Custom Parameters"), help=_( - "Add the key/value pair for any custom parameters, such as the page your e-book should open to or " - "the background color for this component. Ex. [\"page=1\", \"color=white\"]" - "
See the {docs_anchor_open}edX LTI documentation{anchor_close} for more details on this setting." + 'Enter key-value pairs to send with each launch e.g. ["page=1", "color=white"].' ).format( docs_anchor_open=DOCS_ANCHOR_TAG_OPEN, anchor_close="" @@ -478,12 +449,9 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): scope=Scope.settings ) launch_target = String( - display_name=_("LTI Launch Target"), + display_name=_("Open tool in"), help=_( - "Select Inline if you want the LTI content to open in an IFrame in the current page. " - "Select Modal if you want the LTI content to open in a modal window in the current page. " - "Select New Window if you want the LTI content to open in a new browser window. " - "This setting is only used when Hide External Tool is set to False." + "Choose how the tool opens for learners: inline in the page, in a modal, or in a new window." ), default=LaunchTarget.IFRAME.value, scope=Scope.settings, @@ -494,57 +462,47 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): ], ) button_text = String( - display_name=_("Button Text"), + display_name=_("Launch Button Text"), help=_( - "Enter the text on the button used to launch the third party application. " - "This setting is only used when Hide External Tool is set to False and " - "LTI Launch Target is set to Modal or New Window." + "Enter the label shown on the launch button when the tool opens in a modal or new window." ), default="", scope=Scope.settings ) inline_height = Integer( - display_name=_("Inline Height"), + display_name=_("Inline Height (px)"), help=_( - "Enter the desired pixel height of the iframe which will contain the LTI tool. " - "This setting is only used when Hide External Tool is set to False and " - "LTI Launch Target is set to Inline." + "Enter the height of the inline iframe in pixels." ), default=800, scope=Scope.settings ) modal_height = Integer( - display_name=_("Modal Height"), + display_name=_("Modal Height (%)"), help=_( - "Enter the desired viewport percentage height of the modal overlay which will contain the LTI tool. " - "This setting is only used when Hide External Tool is set to False and " - "LTI Launch Target is set to Modal." + "Enter the modal height as a percentage of the browser window." ), default=80, scope=Scope.settings ) modal_width = Integer( - display_name=_("Modal Width"), + display_name=_("Modal Width (%)"), help=_( - "Enter the desired viewport percentage width of the modal overlay which will contain the LTI tool. " - "This setting is only used when Hide External Tool is set to False and " - "LTI Launch Target is set to Modal." + "Enter the modal width as a percentage of the browser window." ), default=80, scope=Scope.settings ) has_score = Boolean( - display_name=_("Scored"), - help=_("Select True if this component will receive a numerical score from the external LTI system."), + display_name=_("This activity is graded"), + help=_("Enable this if the tool sends a numeric score back to Open edX."), default=False, scope=Scope.settings ) weight = Float( - display_name="Weight", + display_name=_("Grade Weight"), help=_( - "Enter the number of points possible for this component. " - "The default value is 1.0. " - "This setting is only used when Scored is set to True." + "Enter the maximum number of points for this component." ), default=1.0, scope=Scope.settings, @@ -563,16 +521,14 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): hide_launch = Boolean( display_name=_("Hide External Tool"), help=_( - "Select True if you want to use this component as a placeholder for syncing with an external grading " - "system rather than launch an external tool. " - "This setting hides the Launch button and any IFrames for this component." + "Enable this to hide launch button and iframe so you use the component for grade sync etc." ), default=False, scope=Scope.settings ) accept_grades_past_due = Boolean( - display_name=_("Accept grades past deadline"), - help=_("Select True to allow third party systems to post grades past the deadline."), + display_name=_("Accept grades after due date"), + help=_("Enable this to allow the tool to send grades after the due date has passed."), default=True, scope=Scope.settings ) @@ -580,31 +536,30 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): # party application. When "Open in New Page" is not selected, the tool automatically appears without any # user action. ask_to_send_username = Boolean( - display_name=_("Request user's username"), + display_name=_("Share Username"), # Translators: This is used to request the user's username for a third party service. - help=_("Select True to request the user's username."), + help=_("Enable this to send the learner’s username to the tool."), default=False, scope=Scope.settings ) ask_to_send_full_name = Boolean( - display_name=_("Request user's full name"), + display_name=_("Share Full name"), # Translators: This is used to request the user's full name for a third party service. - help=_("Select True to request the user's full name."), + help=_("Enable this to send the learner’s full name to the tool."), default=False, scope=Scope.settings ) ask_to_send_email = Boolean( - display_name=_("Request user's email"), + display_name=_("Share Email"), # Translators: This is used to request the user's email for a third party service. - help=_("Select True to request the user's email address."), + help=_("Enable this to send the learner’s email address to the tool."), default=False, scope=Scope.settings ) enable_processors = Boolean( display_name=_("Send extra parameters"), - help=_("Select True to send the extra parameters, which might contain Personally Identifiable Information. " - "The processors are site-wide, please consult the site administrator if you have any questions."), + help=_("Enable this to send extra parameters (may contain PII). Consult site admin for details."), default=False, scope=Scope.settings ) @@ -1195,14 +1150,44 @@ def studio_view(self, context): Get Studio View fragment """ loader = ResourceLoader(__name__) - fragment = super().studio_view(context) - fragment.add_javascript(loader.load_unicode("static/js/xblock_studio_view.js")) + course = self.course + lti_passport_ids = [lti_passport.split(':')[0].strip() for lti_passport in course.lti_passports] + + context = { + 'fields': {}, + 'pii_sharing_enabled': self.get_pii_sharing_enabled(), + 'lti_passports': lti_passport_ids, + } + + # Add editable fields to context + for field_name in self.editable_fields: + field = self.fields[field_name] # pylint: disable=unsubscriptable-object + field_info = self._make_field_info(field_name, field) + if field_info is not None: + context["fields"][field_name] = field_info + + fragment = Fragment() + fragment.add_content(loader.render_django_template( + '/templates/html/lti_studio_edit.html', + context=context, + i18n_service=self.runtime.service(self, 'i18n') + )) + + fragment.add_css(loader.load_unicode('static/css/xblock_studio_view.css')) + fragment.add_javascript(loader.load_unicode('static/js/xblock_studio_view.js')) + js_context = { "EXTERNAL_MULTIPLE_LAUNCH_URLS_ENABLED": external_multiple_launch_urls_enabled( self.scope_ids.usage_id.course_key - ) + ), + "editableFields": self.editable_fields, } + + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + fragment.add_javascript_url(statici18n_js_url) + fragment.initialize_js('LtiConsumerXBlockInitStudio', js_context) return fragment diff --git a/lti_consumer/static/css/xblock_studio_view.css b/lti_consumer/static/css/xblock_studio_view.css new file mode 100644 index 00000000..3c58b771 --- /dev/null +++ b/lti_consumer/static/css/xblock_studio_view.css @@ -0,0 +1,331 @@ +.pgn__stepper-header { + display: flex; + flex-direction: row !important; + flex: none !important; + justify-content: center; + align-items: center; + background: #00000000; + padding: 0.75rem 1rem; + height: 5.13rem; + + .pgn__stepper-header-step-list { + list-style:none; + padding: 0.25rem 0; + display: flex; + align-items: center; + margin: 0; + flex-grow: 1; + justify-content: center; + + .pgn__stepper-header-line { + display: block; + height: 1px; + background: #E1DDDB; + flex-basis: 80px; + margin: 0 .5rem; + } + } + + .pgn__stepper-header-step { + display: flex; + align-items: center; + color: #0A3055; + flex-shrink: 1; + min-width: 0; + padding: 0.25rem; + + .pgn__stepper-header-step-title { + white-space: nowrap; + overflow: hidden; + min-width: 0; + text-overflow: ellipsis; + } + + + &.pgn__stepper-header-step-active { + .pgn__bubble { + background: #0A3055; + } + } + + &:not(.pgn__stepper-header-step-active) { + .pgn__bubble { + background: #707070; + } + } + + &.pgn__stepper-header-step-active ~ .pgn__stepper-header-step { + color: #707070; + .pgn__bubble { + background: #707070; + } + } + } + + .pgn__bubble { + line-height: normal; + height: 1.5rem; + width: 1.5rem; + border-radius: 50%; + display: inline-flex !important; + margin-inline-end: .5rem; + align-items: center; + justify-content: center; + font-size: 75%; + color: #FFFFFF; + } +} + +.hidden { + display: none !important; +} + +.button-select-options { + list-style-type: none; + padding: 0; + margin-top: 0.5rem; + display: flex; + + li { + display: inline-block; + width: 100px; + height: 40px; + margin: 0px !important; + position: relative; + flex-grow: 1; + + &:first-child label { + border-radius: 4px 0 0 4px; + } + + &:last-child label { + border-radius: 0 4px 4px 0; + } + + &:not(:first-child) label { + /* Remove the left border of all but the first label to avoid double borders. */ + border-left: none; + } + } + + input { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + cursor: pointer; + } + + label { + display: block; + padding: 5px; + border: 1px solid #0a3055; + cursor: pointer; + text-align: center; + font-weight: 500 !important; + font-size: 18px !important; + } + + input[type="radio"] { + opacity: 0; + } + + input[type="radio"]:checked+label { + background: #0a3055; + color: #fff !important; + } +} + +.field-group { + flex-grow: 0 !important; + + &.field-group-lti-configuration { + ul { + display: flex; + flex-direction: row !important; + } + + li:not(:last-child) { + margin-right: 2rem; + } + + li.lti-version { + width: 200px; + } + + li.flex-grow { + flex-grow: 1; + } + } + + .field-group-heading { + font-weight: 700; + font-size: 12px; + line-height: 20px; + color: #454545; + margin-bottom: 0.5rem; + } + + .two-column { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + gap: 1rem; + + li { + flex-grow: 1; + } + } +} + +hr.field-group-separator { + border: none; + border-top: 2px solid #E9E6E4; + margin: 1rem 0; + width: 100% !important; +} + +.step-body { + padding: 1rem; + flex-grow: 0; + height: 315px; + overflow-y: scroll; +} + +span.icon.fa-question-circle { + color: #707070; +} + +div.tooltip { + min-width: 300px; +} + +[class*="view-"] { + .wrapper .modal-window.modal-editor, + .wrapper.xblock-iframe-content { + &:not(.modal-fullscreen) { + width: 742px; + } + + .modal-content { + display: flex; + flex-direction: column; + flex-grow: 1; + + .xblock-editor { + display: flex; + flex-direction: column; + flex-grow: 1; + + .xblock-studio_view { + display: flex; + flex-direction: column; + flex-grow: 1; + + .editor-with-buttons { + display: flex; + flex-direction: column; + margin-bottom: 0; + flex-grow: 1; + } + + .xblock-actions { + flex: none; + margin-top: auto; + flex-direction: row; + position: relative; + + .action-buttons-left { + margin-right: auto; + margin-left: 0; + margin-bottom: 0; + } + + .action-buttons { + flex-grow: 0; + } + + .primary-button { + color: #fff; + background-color: #0a3055; + box-shadow: none; + font-weight: 500; + border: 1px solid transparent; + padding: 10px 16px; + font-size: 18px; + line-height: 20px; + border-radius: 6px; + text-align: center; + vertical-align: middle; + user-select: none; + background-image: none; + height: auto; + display: block; + margin-right: 0; + } + + .secondary-button { + color: #333333; + background-color: transparent; + box-shadow: none; + font-weight: 500; + border: 1px solid #707070; + padding: 10px 16px; + font-size: 18px; + line-height: 20px; + border-radius: 6px; + text-align: center; + vertical-align: middle; + user-select: none; + background-image: none; + display: block; + } + } + } + } + } + + .comp-setting-entry { + margin-top: 0.5rem; + } + + label:not(.checkbox-label), + .label.setting-label:not(.checkbox-label) { + font-weight: 700; + font-size: 14px; + line-height: 32px; + color: #082644; + } + + select, + textarea, + input[type="text"] { + display: block; + margin-top: 0.5rem; + width: 100%; + } + + select { + border: 1px solid #707070; + border-radius: 6px; + padding: 10px 16px; + font-size: 18px; + line-height: 20px; + color: #082644; + font-weight: 500; + font-size: 18px; + + option { + color: #0a3055; + font-weight: bold; + } + } + + textarea { + resize: none; + height: auto; + } + } +} diff --git a/lti_consumer/static/js/xblock_studio_view.js b/lti_consumer/static/js/xblock_studio_view.js index ef790c93..42d62c66 100644 --- a/lti_consumer/static/js/xblock_studio_view.js +++ b/lti_consumer/static/js/xblock_studio_view.js @@ -1,178 +1,486 @@ /** * Javascript for LTI Consumer Studio View. -*/ + */ function LtiConsumerXBlockInitStudio(runtime, element, data) { - // Run parent function to set up studio view base JS - StudioEditableXBlockMixin(runtime, element); - - // Define LTI 1.1 and 1.3 fields - const lti1P1FieldList = [ - "lti_id", - "launch_url" - ]; - - const lti1P3FieldList = [ - "lti_1p3_launch_url", - "lti_1p3_redirect_uris", - "lti_1p3_oidc_url", - "lti_1p3_tool_key_mode", - "lti_1p3_tool_keyset_url", - "lti_1p3_tool_public_key", - "lti_advantage_ags_mode", - "lti_advantage_deep_linking_enabled", - "lti_advantage_deep_linking_launch_url", - "lti_1p3_enable_nrps" - ]; - - /** - * Query a field using the `data-field-name` attribute and hide/show it. - * - * params: - * field: string. Value of the field's `data-field-name` attribute. - * visible: boolean. `true` shows the container, and `false` hides it. - */ - function toggleFieldVisibility(field, visible) { - const componentQuery = '[data-field-name="' + field + '"]'; - const fieldContainer = element.find(componentQuery); - - if (visible) { - fieldContainer.show(); - } else { - fieldContainer.hide(); - } + let currentStep = "setup"; + + /** + * Query a field using the `data-field-name` attribute and hide/show it. + * + * @param {string} field - Value of the field's `data-field-name` attribute. + * @param {boolean} visible - `true` shows the container, and `false` hides it. + */ + function toggleFieldVisibility(field, visible) { + const componentQuery = '[data-field-name="' + field + '"]'; + const fieldContainer = element.find(componentQuery); + + if (visible) { + fieldContainer.removeClass("hidden"); + } else { + fieldContainer.addClass("hidden"); } + } + + /** + * Return the value of the selected radio button. + * @param {string} fieldName - The name of the field to search for. + * @returns {string} The value of the selected radio button. + */ + function getRadioButtonValue(fieldName) { + const options = $(element).find(`input[id^=${fieldName}_option-]`); - /** - * Return fields that should be hidden based on the selected lti version. - */ - function getFieldsToHideForLtiVersion() { - const ltiVersionField = $(element).find('#xb-field-edit-lti_version'); - const selectedVersion = ltiVersionField.children("option:selected").val(); - const fieldsToHide = []; - - if (selectedVersion === undefined || selectedVersion === "lti_1p1") { - // If LTI version field isn't present, then LTI 1.3 support is disabled - // so hide all LTI 1.3 fields. If the LTI version is LTI 1.1, also hide all LTI - // 1.3 fields. - lti1P3FieldList.forEach(function (field) { - fieldsToHide.push(field); - }); - } else if (selectedVersion === "lti_1p3") { - lti1P1FieldList.forEach(function (field) { - fieldsToHide.push(field); - }); - } else { } - - return fieldsToHide; + for (let i = 0; i < options.length; i++) { + if (options[i].checked) { + return options[i].value; + } } + throw new Error(`No option selected for ${fieldName}`); + } - /** - * Return fields that should be hidden based on the selected config type. - * - * new - Show all the LTI 1.1/1.3 config fields - * database - Do not show the LTI 1.1/1.3 config fields - * external - Show only the External Config ID field - */ - function getFieldsToHideForLtiConfigType() { - const configType = $(element).find('#xb-field-edit-config_type').val(); - const databaseConfigHiddenFields = lti1P1FieldList.concat(lti1P3FieldList); - const externalConfigHiddenFields = lti1P1FieldList.concat(lti1P3FieldList); - const fieldsToHide = []; - - if (configType === "external") { - // Hide LTI 1.1 and LTI 1.3 tool fields. - externalConfigHiddenFields.forEach(function (field) { - fieldsToHide.push(field); - }) - // Conditionally show the LTI 1.3 launch URL field if external multiple launch URLs are enabled. - if (data.EXTERNAL_MULTIPLE_LAUNCH_URLS_ENABLED) { - const index = fieldsToHide.indexOf("lti_1p3_launch_url"); - if (index > -1) { - fieldsToHide.splice(index, 1); - } - } - } else if (configType === "database") { - // Hide the LTI 1.1 and LTI 1.3 fields. The XBlock will remain the source of truth for the lti_version, - // so do not hide it and continue to allow editing it from the XBlock edit menu in Studio. - databaseConfigHiddenFields.forEach(function (field) { - fieldsToHide.push(field); - }) - } else { - // No fields should be hidden based on a config_type of 'new'. - } + /** + * Show or hide components depending on the selected lti_version and config_type. + */ + function toggleLtiComponents() { + const version = getRadioButtonValue("lti_version"); + let configType; + try { + configType = getFieldValue("config_type").value || "new"; + } catch (e) { + // The waffle flag is not enabled, so we default to "new". + configType = "new"; + } + + if (version === "lti_1p1") { + $(element).find(".l1p1").removeClass("hidden"); + $(element).find(".l1p3").addClass("hidden"); + } else { + $(element).find(".l1p1").addClass("hidden"); + $(element).find(".l1p3").removeClass("hidden"); - return fieldsToHide; } - /** - * Return fields that should be hidden based on the selected key mode. This returns a list of of fields related to - * lti tool key mode that should be hidden. - */ - function getFieldsToHideForLtiToolKeyMode() { - const ltiKeyModeField = $(element).find('#xb-field-edit-lti_1p3_tool_key_mode'); - const selectedKeyMode = ltiKeyModeField.children("option:selected").val(); - const fieldsToHide = []; - - if (selectedKeyMode === 'public_key') { - fieldsToHide.push("lti_1p3_tool_keyset_url"); - } else if (selectedKeyMode === 'keyset_url') { - fieldsToHide.push("lti_1p3_tool_public_key"); - } + if (configType === "new") { + toggleFieldVisibility("lti_id", version === "lti_1p1"); + toggleFieldVisibility("external_config", false); + } else { + toggleFieldVisibility("lti_id", false); + toggleFieldVisibility("external_config", true); + } - return fieldsToHide; + if (configType === "external") { + // Conditionally show the LTI 1.3 launch URL field if external multiple launch URLs are enabled. + toggleFieldVisibility("lti_1p3_launch_url", data.EXTERNAL_MULTIPLE_LAUNCH_URLS_ENABLED); } + } - /** - * Show or hide fields depending on the selected lti_version, config_type, and lti_1p3_tool_key_mode. - */ - function toggleLtiFields() { - const configFields = lti1P1FieldList.concat(lti1P3FieldList); - const hiddenFields = new Set(); - - // Start with the assumption that all configFields should be visible. After that, we whittle down the - // list of visible fields based on the values of those fields. - configFields.forEach(function (field) { - toggleFieldVisibility( - field, - true - ); - }); - - let fieldsToHide; - const hiddenFieldsFilters = [ - getFieldsToHideForLtiVersion, - getFieldsToHideForLtiConfigType, - getFieldsToHideForLtiToolKeyMode - ]; - - hiddenFieldsFilters.forEach(function (filter) { - fieldsToHide = filter(); - - fieldsToHide.forEach(function (field) { - hiddenFields.add(field); - }) - }) - - for (const field of hiddenFields) { - toggleFieldVisibility(field, false); - } + /* + * Show or hide fields depending on the lti_advantage_deep_linking_enabled option. + */ + function toggleDeepLinking() { + const enabled = getRadioButtonValue("lti_advantage_deep_linking_enabled") === "true"; + + $(element).find("[data-field-name=lti_advantage_deep_linking_launch_url]").toggleClass("hidden", !enabled); + } + + /** + * Show or hide fields depending on the selected has_score option. + */ + function toggleHasScore() { + const hasScore = getRadioButtonValue("has_score") === "true"; + const fields = ["weight", "accept_grades_past_due"]; + + fields.forEach((field) => toggleFieldVisibility(field, hasScore)); + } + + /** + * Show or hide fields depending on the selected hide_launch and launch_target options. + */ + function toggleHideExternalTool() { + const hideExternalTool = getRadioButtonValue("hide_launch") === "true"; + const fields = { + inline_height: ["iframe"], + modal_height: ["modal"], + modal_width: ["modal"], + button_text: ["modal", "new_window"], + }; + + const launchTarget = getRadioButtonValue("launch_target"); + + if (hideExternalTool) { + // Hide all related fields + [ + "launch_target", + ...Object.keys(fields), // Hide all fields in the object + ].forEach((field) => toggleFieldVisibility(field, false)); + } else { + toggleFieldVisibility("launch_target", true); + + // Set visibility according to the launch_target + Object.entries(fields).forEach(([field, targets]) => + toggleFieldVisibility(field, targets.includes(launchTarget)), + ); + } + } + + /** + * Show or hide fields related to the selected lti_1p3_tool_key_mode option. + */ + function toggleLti1p3ToolKeyMode() { + const deepLinkingEnabled = getRadioButtonValue("lti_advantage_deep_linking_enabled") === "true"; + const nprsEnabled = getRadioButtonValue("lti_1p3_enable_nrps") === "true"; + const agsModeEnabled = getRadioButtonValue("lti_advantage_ags_mode") !== "disabled"; + + if (deepLinkingEnabled || nprsEnabled || agsModeEnabled) { + $(element).find("[data-field-name=lti_1p3_tool_key_mode]").removeClass("hidden"); + + const keyMode = getRadioButtonValue("lti_1p3_tool_key_mode"); + + if (keyMode === "public_key") { + $(element).find("[data-field-name=lti_1p3_tool_keyset_url]").addClass("hidden"); + $(element).find("[data-field-name=lti_1p3_tool_public_key]").removeClass("hidden"); + } else if (keyMode === "keyset_url") { + $(element).find("[data-field-name=lti_1p3_tool_keyset_url]").removeClass("hidden"); + $(element).find("[data-field-name=lti_1p3_tool_public_key]").addClass("hidden"); + } else { + throw new Error("This should never happen"); + } + } else { + $(element).find("[data-field-name=lti_1p3_tool_key_mode]").addClass("hidden"); + $(element).find("[data-field-name=lti_1p3_tool_keyset_url]").addClass("hidden"); + $(element).find("[data-field-name=lti_1p3_tool_public_key]").addClass("hidden"); } + } + + /** + * Hide the current step and deactivate the step header. + * + * @param {string} step - The step to deactivate. + */ + function deactivateCurrentStep() { + $(element).find(`.step-header-${currentStep}`).removeClass("pgn__stepper-header-step-active"); + $(element).find(`.step-${currentStep}`).addClass("hidden"); + }; + + /** + * Show the current step and activate the step header. + * + * @param {string} step - The step to activate. + */ + function activateCurrentStep() { + $(element).find(`.step-header-${currentStep}`).addClass("pgn__stepper-header-step-active"); + $(element).find(`.step-${currentStep}`).removeClass("hidden"); + } + + /** + * Change the current step. + * + * @param {string} step - The step to change to. + */ + function changeStep(step) { + deactivateCurrentStep(); + currentStep = step; + activateCurrentStep(); + handlePrevNextButtonVisibility(); + } + + // Show or hide fields based on the selected options + toggleLtiComponents(); + toggleDeepLinking(); + toggleLti1p3ToolKeyMode(); + toggleHasScore(); + toggleHideExternalTool(); + + // Bind events to input/select fields to mark the field as set + $(element) + .find(".field-data-control") + .bind("change input paste", function () { + // Add a class to the field to indicate that the value has been changed + const wrapper = $(this).closest("li.field"); + $(wrapper).addClass("is-set"); + }); + + // Bind events to radio fields to mark the field as set + $(element) + .find("input[type=radio]") + .bind("change", function () { + // Add a class to the field to indicate that the value has been changed + const wrapper = $(this).closest("li.field"); + $(wrapper).addClass("is-set"); + }); - // Call once component is instanced to hide fields - toggleLtiFields(); - // Bind to onChange method of lti_version selector - $(element).find('#xb-field-edit-lti_version').bind('change', function () { - toggleLtiFields(); + // Bind to onChange method of lti_version selector + $(element) + .find("[id^=lti_version_option-]") + .bind("change", function () { + toggleLtiComponents(); }); - // Bind to onChange method of lti_1p3_tool_key_mode selector - $(element).find('#xb-field-edit-lti_1p3_tool_key_mode').bind('change', function () { - toggleLtiFields(); + // Bind to onChange method of lti_advantage_deep_linking_enabled selector + $(element) + .find("[id^=lti_advantage_deep_linking_enabled_option-]") + .bind("change", function () { + toggleDeepLinking(); + toggleLti1p3ToolKeyMode(); }); - $(element).find('#xb-field-edit-config_type').bind('change', function () { - toggleLtiFields(); + // Bind to onChange method of lti_1p3_enable_nrps selector + $(element) + .find("[id^=lti_1p3_enable_nrps_option-]") + .bind("change", function () { + toggleLti1p3ToolKeyMode(); + }); + + // Bind to onChange method of lti_advantage_ags_mode selector + $(element) + .find("[id^=lti_advantage_ags_mode_option-]") + .bind("change", function () { + toggleLti1p3ToolKeyMode(); + }); + + // Bind to onChange method of lti_1p3_tool_key_mode selector + $(element) + .find("[id^=lti_1p3_tool_key_mode_option-]") + .bind("change", function () { + toggleLti1p3ToolKeyMode(); + }); + + // Bind to onChange method of has_score selector + $(element) + .find("[id^=has_score_option-]") + .bind("change", function () { + toggleHasScore(); + }); + + // Bind to onChange method of hide_launch selector + $(element) + .find("[id^=hide_launch_option-]") + .bind("change", function () { + toggleHideExternalTool(); + }); + + // Bind to onChange method of launch_target selector + $(element) + .find("[id^=launch_target_option-]") + .bind("change", function () { + toggleHideExternalTool(); + }); + + // Bind to onChange method of lti_1p3_tool_key_mode selector + $(element) + .find("[id^=lti_1p3_tool_key_mode_option-]") + .bind("change", function () { + toggleLti1p3ToolKeyMode(); + }); + + // Bind to onChange method of config_type selector + $(element) + .find("[id^=xb-field-edit-config_type]") + .bind("change", function () { + toggleLtiComponents(); + }); + + $(element) + .find(".cancel-button") + .bind("click", function () { + runtime.notify("cancel", {}); + }); + + function handlePrevNextButtonVisibility() { + if (currentStep === "setup") { + $(element).find(".previous-button").closest("li").addClass("hidden"); + } else { + $(element).find(".previous-button").closest("li").removeClass("hidden"); + } + + if (currentStep === "review") { + $(element).find(".next-button").closest("li").addClass("hidden"); + $(element).find(".save-button").closest("li").removeClass("hidden"); + } else { + $(element).find(".next-button").closest("li").removeClass("hidden"); + $(element).find(".save-button").closest("li").addClass("hidden"); + } + } + + $(element) + .find(".next-button") + .bind("click", function () { + let nextStep; + const version = getRadioButtonValue("lti_version"); + + if (currentStep === "setup") { + if (version === "lti_1p1") { + nextStep = "review"; + } else { + nextStep = "advantage"; + } + } else if (currentStep === "advantage") { + nextStep = "review"; + } else if (currentStep === "review") { + throw new Error("This should never happen"); + } + changeStep(nextStep); + }); + + $(element) + .find(".previous-button") + .bind("click", function () { + let previousStep; + const version = getRadioButtonValue("lti_version"); + + if (currentStep === "setup") { + throw new Error("This should never happen"); + } else if (currentStep === "advantage") { + previousStep = "setup"; + } else if (currentStep === "review") { + if (version === "lti_1p1") { + previousStep = "setup"; + } else { + previousStep = "advantage"; + } + } + changeStep(previousStep); + }); + + $(element) + .find(".step-header-setup-link") + .bind("click", function () { + changeStep("setup"); + }); + + $(element) + .find(".step-header-advantage-link") + .click("click", function () { + changeStep("advantage"); + }); + + $(element) + .find(".step-header-review-link") + .bind("click", function () { + changeStep("review"); + }); + + + /** + * Return whether the field is set or not. + * + * @param {Element} field - The field to check. + * @returns {boolean} Whether the field is set or not. + */ + function getIsSet(field) { + const wrapper = $(field).closest("li.field"); + return $(wrapper).hasClass("is-set"); + } + + /** + * Return the value of the field, or `null` if the field is not set. + * + * @param {string} fieldName - The name of the field to get the value of. + * @returns {{isSet: boolean, value: string | null}} The value of the field, or `null` if the field is not set. + */ + function getFieldValue(fieldName) { + const field = $(element).find(`#xb-field-edit-${fieldName}`); + let value; + let isSet; + + if (field.length === 0) { + // This is not a text/select field, so we get the value of the select option. + options = $(element).find(`input[id^=${fieldName}_option-]`); + if (options.length === 0) { + throw new Error(`No options for ${fieldName}`); + } + isSet = getIsSet(options[0]); + value = isSet ? getRadioButtonValue(fieldName) : null; + } else { + isSet = getIsSet(field); + if (field.attr("type") === "checkbox") { + value = field.prop("checked"); + } else { + value = field.val(); + } + } + + return { + isSet, + value, + }; + } + + $(element) + .find(".save-button") + .bind("click", function () { + const { editableFields } = data; + const handlerUrl = runtime.handlerUrl(element, "submit_studio_edits"); + const submitData = { values: {}, defaults: [] }; + for (const field of editableFields) { + const { isSet, value } = getFieldValue(field); + if (isSet) { + submitData.values[field] = value; + } else { + submitData.defaults.push(field); + } + } + + // Transform the custom_parameters field into a list + if ("custom_parameters" in submitData.values) { + const customParameters = submitData.values.custom_parameters; + try { + submitData.values.custom_parameters = JSON.parse(customParameters); + } catch (e) { + runtime.notify("error", { + title: gettext("Unable to update settings"), + message: gettext("Unable to parse the custom parameters."), + }); + return; + } + submitData.values.custom_parameters = JSON.parse(customParameters); + } + + // Transform the lti_1p3_redirect_uris field into a list + if ("lti_1p3_redirect_uris" in submitData.values) { + const redirectUris = submitData.values.lti_1p3_redirect_uris; + try { + submitData.values.lti_1p3_redirect_uris = JSON.parse(redirectUris); + } catch (e) { + runtime.notify("error", { + title: gettext("Unable to update settings"), + message: gettext("Unable to parse the redirect URIs."), + }); + return; + } + } + + runtime.notify("save", { state: "start" }); + + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify(submitData), + dataType: "json", + contentType: "application/json", + global: false, + success: function () { + runtime.notify("save", { state: "end" }); + }, + }).fail(function (jqXHR) { + var message = gettext( + "This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.", + ); + if (jqXHR.responseText) { + try { + message = JSON.parse(jqXHR.responseText).error; + if (typeof message === "object" && message.messages) { + // e.g. {"error": {"messages": [{"text": "Unknown user 'bob'!", "type": "error"}, ...]}} etc. + message = $.map(message.messages, function (msg) { + return msg.text; + }).join(", "); + } + } catch (error) { + message = jqXHR.responseText.substr(0, 300); + } + } + runtime.notify("error", { title: gettext("Unable to update settings"), message: message }); + }); }); } diff --git a/lti_consumer/templates/html/lti_studio_edit.html b/lti_consumer/templates/html/lti_studio_edit.html new file mode 100644 index 00000000..8d164de2 --- /dev/null +++ b/lti_consumer/templates/html/lti_studio_edit.html @@ -0,0 +1,755 @@ +{% load i18n %} +
+ +
+
+
+
+ {% trans "LTI Configuration" %} +
+
    + + {% if fields.config_type.values %} +
  • + + + +
  • + {% endif %} + + +
  • + + +
      + {% spaceless %} + {% for option in fields.lti_version.values %} +
    • + + +
    • + {% endfor %} + {% endspaceless %} +
    +
  • + + +
  • + + + +
  • + + + {% if fields.external_config %} +
  • + + + +
  • + {% endif %} + +
+
+
+
+
+ {% trans "LTI 1.1/1.2 Configuration" %} +
+
    + +
  • + + + +
  • + +
+
+
+
+ {% trans "LTI 1.3 Configuration" %} +
+
    + +
  • + + + +
  • + + + +
  • + + + +
  • + + +
  • + + + +
  • + +
+
+
+ + +
+ +
diff --git a/lti_consumer/tests/unit/test_lti_xblock.py b/lti_consumer/tests/unit/test_lti_xblock.py index a97138b3..7c7abd89 100644 --- a/lti_consumer/tests/unit/test_lti_xblock.py +++ b/lti_consumer/tests/unit/test_lti_xblock.py @@ -1971,10 +1971,20 @@ def test_get_context_title(self, mock_get_course_by_id): self.assertEqual(self.xblock.get_context_title(), "DemoX - edX") - def test_studio_view(self): + @patch('lti_consumer.plugin.compat.get_course_by_id') + def test_studio_view(self, mock_get_course_by_id): """ Test that the studio settings view load the custom js. """ + # Mock i18n service before fetching author view + self.xblock.runtime.service.return_value = None + + mock_course = Mock() + mock_course.display_name_with_default = "DemoX" + mock_course.display_org_with_default = "edX" + mock_course.lti_passports = ["lti_passport:key:secret"] + mock_get_course_by_id.return_value = mock_course + response = self.xblock.studio_view({}) self.assertEqual(response.js_init_fn, 'LtiConsumerXBlockInitStudio') From e1dedc1049bbf3dc550ec721d9262c09931888b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 22 Apr 2026 16:02:57 -0300 Subject: [PATCH 02/21] fix: self-closing span and li --- .../templates/html/lti_studio_edit.html | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/lti_consumer/templates/html/lti_studio_edit.html b/lti_consumer/templates/html/lti_studio_edit.html index 8d164de2..1b402994 100644 --- a/lti_consumer/templates/html/lti_studio_edit.html +++ b/lti_consumer/templates/html/lti_studio_edit.html @@ -12,7 +12,7 @@ -
  • @@ -23,7 +23,7 @@
  • -
  • @@ -51,7 +51,7 @@
    data-field-name="config_type" > - + data-field-name="external_config" > - +
  • - +
  • @@ -160,7 +160,7 @@
  • - +
  • - +
  • - + -
  • - - -
    - - -
    - -
    - - + {{ step_setup_html|safe }} + {{ step_advantage_html|safe }} + {{ step_review_html|safe }}
    + {{ actions_html|safe }} diff --git a/lti_consumer/templates/html/lti_studio_edit/_actions.html b/lti_consumer/templates/html/lti_studio_edit/_actions.html new file mode 100644 index 00000000..9f4a5a24 --- /dev/null +++ b/lti_consumer/templates/html/lti_studio_edit/_actions.html @@ -0,0 +1,19 @@ +{% load i18n %} + diff --git a/lti_consumer/templates/html/lti_studio_edit/_step_advantage.html b/lti_consumer/templates/html/lti_studio_edit/_step_advantage.html new file mode 100644 index 00000000..735fff04 --- /dev/null +++ b/lti_consumer/templates/html/lti_studio_edit/_step_advantage.html @@ -0,0 +1,189 @@ +{% load i18n %} + diff --git a/lti_consumer/templates/html/lti_studio_edit/_step_review.html b/lti_consumer/templates/html/lti_studio_edit/_step_review.html new file mode 100644 index 00000000..8ce7a611 --- /dev/null +++ b/lti_consumer/templates/html/lti_studio_edit/_step_review.html @@ -0,0 +1,351 @@ +{% load i18n %} + diff --git a/lti_consumer/templates/html/lti_studio_edit/_step_setup.html b/lti_consumer/templates/html/lti_studio_edit/_step_setup.html new file mode 100644 index 00000000..fc9fe7c9 --- /dev/null +++ b/lti_consumer/templates/html/lti_studio_edit/_step_setup.html @@ -0,0 +1,160 @@ +{% load i18n %} +
    +
    +
    + {% trans "LTI Configuration" %} +
    +
      + + {% if fields.config_type.values %} +
    • + + + +
    • + {% endif %} + + +
    • + + +
        + {% spaceless %} + {% for option in fields.lti_version.values %} +
      • + + +
      • + {% endfor %} + {% endspaceless %} +
      +
    • + + +
    • + + + +
    • + + + {% if fields.external_config %} +
    • + + + +
    • + {% endif %} + +
    +
    +
    +
    +
    + {% trans "LTI 1.1/1.2 Configuration" %} +
    +
      + +
    • + + + +
    • + +
    +
    +
    +
    + {% trans "LTI 1.3 Configuration" %} +
    +
      + +
    • + + + +
    • + + + +
    • + + + +
    • + + +
    • + + + +
    • + +
    +
    +
    diff --git a/lti_consumer/templates/html/lti_studio_edit/_stepper_header.html b/lti_consumer/templates/html/lti_studio_edit/_stepper_header.html new file mode 100644 index 00000000..8764ddba --- /dev/null +++ b/lti_consumer/templates/html/lti_studio_edit/_stepper_header.html @@ -0,0 +1,38 @@ +{% load i18n %} + From 46d4567de4c75ff6b31ed4e3d678a100215c86ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 28 Apr 2026 15:20:27 -0300 Subject: [PATCH 10/21] feat: hide fields if external config selected --- lti_consumer/static/js/xblock_studio_view.js | 39 ++++++++++++------- .../html/lti_studio_edit/_step_setup.html | 9 +++-- .../html/lti_studio_edit/_stepper_header.html | 10 ++--- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/lti_consumer/static/js/xblock_studio_view.js b/lti_consumer/static/js/xblock_studio_view.js index 3a907763..e752f8bb 100644 --- a/lti_consumer/static/js/xblock_studio_view.js +++ b/lti_consumer/static/js/xblock_studio_view.js @@ -57,7 +57,13 @@ function LtiConsumerXBlockInitStudio(runtime, element, data) { } else { $(element).find(".l1p1").addClass("hidden"); $(element).find(".l1p3").removeClass("hidden"); - + if (configType !== "new") { + $(element).find(".no-external-config").addClass("hidden"); + $(element).find(".external-config").removeClass("hidden"); + } else { + $(element).find(".external-config").addClass("hidden"); + $(element).find(".no-external-config").removeClass("hidden"); + } } if (configType === "new") { @@ -69,8 +75,15 @@ function LtiConsumerXBlockInitStudio(runtime, element, data) { } if (configType === "external") { - // Conditionally show the LTI 1.3 launch URL field if external multiple launch URLs are enabled. - toggleFieldVisibility("lti_1p3_launch_url", data.EXTERNAL_MULTIPLE_LAUNCH_URLS_ENABLED); + if (data.EXTERNAL_MULTIPLE_LAUNCH_URLS_ENABLED) { + // Conditionally show the LTI 1.3 launch URL field if external multiple launch URLs are enabled. + toggleFieldVisibility("lti_1p3_launch_url", true); + $(element).find(".field-group-lti-configuration-details.l1p3").addClass("hidden"); + } else { + toggleFieldVisibility("lti_1p3_launch_url", false); + // Also hides the field-group-lti-configuration-details as it is empty in this case + $(element).find(".field-group-lti-configuration-details.l1p3").addClass("hidden"); + } } } @@ -158,9 +171,9 @@ function LtiConsumerXBlockInitStudio(runtime, element, data) { * @param {string} step - The step to deactivate. */ function deactivateCurrentStep() { - $(element).find(`.step-header-${currentStep}`).removeClass("pgn__stepper-header-step-active"); - $(element).find(`.step-${currentStep}`).addClass("hidden"); - }; + $(element).find(`.step-header-${currentStep}`).removeClass("pgn__stepper-header-step-active"); + $(element).find(`.step-${currentStep}`).addClass("hidden"); + } /** * Show the current step and activate the step header. @@ -168,8 +181,8 @@ function LtiConsumerXBlockInitStudio(runtime, element, data) { * @param {string} step - The step to activate. */ function activateCurrentStep() { - $(element).find(`.step-header-${currentStep}`).addClass("pgn__stepper-header-step-active"); - $(element).find(`.step-${currentStep}`).removeClass("hidden"); + $(element).find(`.step-header-${currentStep}`).addClass("pgn__stepper-header-step-active"); + $(element).find(`.step-${currentStep}`).removeClass("hidden"); } /** @@ -178,10 +191,10 @@ function LtiConsumerXBlockInitStudio(runtime, element, data) { * @param {string} step - The step to change to. */ function changeStep(step) { - deactivateCurrentStep(); - currentStep = step; - activateCurrentStep(); - handlePrevNextButtonVisibility(); + deactivateCurrentStep(); + currentStep = step; + activateCurrentStep(); + handlePrevNextButtonVisibility(); } // Show or hide fields based on the selected options @@ -209,7 +222,6 @@ function LtiConsumerXBlockInitStudio(runtime, element, data) { $(wrapper).addClass("is-set"); }); - // Bind to onChange method of lti_version selector $(element) .find("[id^=lti_version_option-]") @@ -359,7 +371,6 @@ function LtiConsumerXBlockInitStudio(runtime, element, data) { changeStep("review"); }); - /** * Return whether the field is set or not. * diff --git a/lti_consumer/templates/html/lti_studio_edit/_step_setup.html b/lti_consumer/templates/html/lti_studio_edit/_step_setup.html index fc9fe7c9..86b98dd3 100644 --- a/lti_consumer/templates/html/lti_studio_edit/_step_setup.html +++ b/lti_consumer/templates/html/lti_studio_edit/_step_setup.html @@ -120,7 +120,10 @@
      -
    • +
    • -
    • +
    • -
    • +