diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index c3873d33a595..000000000000 --- a/.dockerignore +++ /dev/null @@ -1,152 +0,0 @@ -# .dockerignore for edx-platform. -# There's a lot here, please try to keep it organized. - -### Files that are not needed in the docker file - -/test_root/ -.git - -### Files private to developers - -# Files that should be git-ignored, but are hand-edited or otherwise valued, -# and so should not be destroyed by "make clean". -# start-noclean -requirements/private.txt -requirements/edx/private.in -requirements/edx/private.txt -lms/envs/private.py -cms/envs/private.py -# end-noclean - -### Python artifacts -**/*.pyc - -### Editor and IDE artifacts -**/*~ -**/*.swp -**/*.orig -**/nbproject -**/.idea/ -**/.redcar/ -**/codekit-config.json -**/.pycharm_helpers/ -**/_mac/* -**/IntelliLang.xml -**/conda_packages.xml -**/databaseSettings.xml -**/diff.xml -**/debugger.xml -**/editor.xml -**/ide.general.xml -**/inspection/Default.xml -**/other.xml -**/packages.xml -**/web-browsers.xml - -### NFS artifacts -**/.nfs* - -### OS X artifacts -**/*.DS_Store -**/.AppleDouble -**/:2e_* -**/:2e# - -### Internationalization artifacts -**/*.mo -**/*.po -**/*.prob -**/*.dup -!**/django.po -!**/django.mo -!**/djangojs.po -!**/djangojs.mo -conf/locale/en/LC_MESSAGES/*.mo -conf/locale/fake*/LC_MESSAGES/*.po -conf/locale/fake*/LC_MESSAGES/*.mo - -### Testing artifacts -**/.testids/ -**/.noseids -**/nosetests.xml -**/.cache/ -**/.coverage -**/.coverage.* -**/coverage.xml -**/cover/ -**/cover_html/ -**/reports/ -**/jscover.log -**/jscover.log.* -**/.pytest_cache/ -**/pytest_task*.txt -**/.tddium* -common/test/data/test_unicode/static/ -test_root/courses/ -test_root/data/test_bare.git/ -test_root/export_course_repos/ -test_root/paver_logs/ -test_root/uploads/ -**/django-pyfs -**/.tox/ -common/test/data/badges/*.png - -### Installation artifacts -**/*.egg-info -**/.pip_download_cache/ -**/.prereqs_cache -**/.vagrant/ -**/node_modules -**/bin/ - -### Static assets pipeline artifacts -**/*.scssc -lms/static/css/ -lms/static/certificates/css/ -cms/static/css/ -common/static/common/js/vendor/ -common/static/common/css/vendor/ -common/static/bundles -**/webpack-stats.json - -### Styling generated from templates -lms/static/sass/*.css -lms/static/sass/*.css.map -lms/static/certificates/sass/*.css -lms/static/themed_sass/ -cms/static/css/ -cms/static/sass/*.css -cms/static/sass/*.css.map -cms/static/themed_sass/ -themes/**/css - -### Logging artifacts -**/log/ -**/logs -**/chromedriver.log -**/ghostdriver.log - -### Celery artifacts ### -**/celerybeat-schedule - -### Unknown artifacts -**/database.sqlite -**/courseware/static/js/mathjax/* -**/flushdb.sh -**/build -/src/ -\#*\# -**/.env/ -openedx/core/djangoapps/django_comment_common/comment_client/python -**/autodeploy.properties -**/.ws_migrations_complete -**/dist -**/*.bak - -# Visual Studio Code -**/.vscode - -# Locally generated PII reports -**/pii_report - -/Dockerfile diff --git a/common/djangoapps/terrain/__init__.py b/.github/workflows/publish-ci-docker-image.yml similarity index 100% rename from common/djangoapps/terrain/__init__.py rename to .github/workflows/publish-ci-docker-image.yml diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d399d38770b9..9867ac72f273 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -73,15 +73,13 @@ jobs: run: | sudo apt-get update && sudo apt-get install libmysqlclient-dev libxmlsec1-dev lynx - # Try to log into DockerHub so that we're less likely to be rate-limited when pulling certain images. - # This will fail on any edx-platform fork which doesn't explicitly define its own DockerHub creds. - # That's OK--if we fail to log in, we'll proceed anonymously, and hope we don't get rate-limited. - - name: Try to log into Docker Hub - uses: docker/login-action@v3.3.0 - continue-on-error: true + # We pull this image a lot, and Dockerhub will rate limit us if we pull too often. + # This is an attempt to cache the image for better performance and to work around that. + # It will cache all pulled images, so if we add new images to this we'll need to update the key. + - name: Cache Docker images + uses: ScribeMD/docker-cache@0.5.0 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + key: docker-${{ runner.os }}-mongo-${{ matrix.mongo-version }} - name: Start MongoDB uses: supercharge/mongodb-github-action@1.11.0 diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index fa2a651f8a28..fdc06e9291d0 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -50,6 +50,10 @@ class StudioHomeSerializer(serializers.Serializer): child=serializers.CharField(), allow_empty=True ) + allowed_organizations_for_libraries = serializers.ListSerializer( + child=serializers.CharField(), + allow_empty=True + ) archived_courses = CourseCommonSerializer(required=False, many=True) can_access_advanced_settings = serializers.BooleanField() can_create_organizations = serializers.BooleanField() @@ -62,7 +66,6 @@ class StudioHomeSerializer(serializers.Serializer): libraries_v2_enabled = serializers.BooleanField() taxonomies_enabled = serializers.BooleanField() taxonomy_list_mfe_url = serializers.CharField() - optimization_enabled = serializers.BooleanField() request_course_creator_url = serializers.CharField() rerun_creator_status = serializers.BooleanField() show_new_library_button = serializers.BooleanField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index 3de536d78092..62b56533878f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView +from organizations import api as org_api from openedx.core.lib.api.view_utils import view_auth_classes from ....utils import get_home_context, get_course_context, get_library_context @@ -51,6 +52,7 @@ def get(self, request: Request): "allow_to_create_new_org": true, "allow_unicode_course_id": false, "allowed_organizations": [], + "allowed_organizations_for_libraries": [], "archived_courses": [], "can_access_advanced_settings": true, "can_create_organizations": true, @@ -62,7 +64,6 @@ def get(self, request: Request): "libraries_v1_enabled": true, "libraries_v2_enabled": true, "library_authoring_mfe_url": "//localhost:3001/course/course-v1:edX+P315+2T2023", - "optimization_enabled": true, "request_course_creator_url": "/request_course_creator", "rerun_creator_status": true, "show_new_library_button": true, @@ -80,7 +81,12 @@ def get(self, request: Request): home_context = get_home_context(request, True) home_context.update({ - 'allow_to_create_new_org': settings.FEATURES.get('ENABLE_CREATOR_GROUP', True) and request.user.is_staff, + # 'allow_to_create_new_org' is actually about auto-creating organizations + # (e.g. when creating a course or library), so we add an additional test. + 'allow_to_create_new_org': ( + home_context['can_create_organizations'] and + org_api.is_autocreate_enabled() + ), 'studio_name': settings.STUDIO_NAME, 'studio_short_name': settings.STUDIO_SHORT_NAME, 'studio_request_email': settings.FEATURES.get('STUDIO_REQUEST_EMAIL', ''), diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index a4a6909c5dcb..8fe246cf23fd 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -8,16 +8,11 @@ from django.conf import settings from django.test import override_settings from django.urls import reverse -from edx_toggles.toggles.testutils import ( - override_waffle_switch, -) from rest_framework import status from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase -from cms.djangoapps.contentstore.views.course import ENABLE_GLOBAL_STAFF_OPTIMIZATION from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory -from xmodule.modulestore.tests.factories import CourseFactory FEATURES_WITH_HOME_PAGE_COURSE_V2_API = settings.FEATURES.copy() @@ -37,9 +32,10 @@ def setUp(self): self.url = reverse("cms.djangoapps.contentstore:v1:home") self.expected_response = { "allow_course_reruns": True, - "allow_to_create_new_org": False, + "allow_to_create_new_org": True, "allow_unicode_course_id": False, "allowed_organizations": [], + "allowed_organizations_for_libraries": [], "archived_courses": [], "can_access_advanced_settings": True, "can_create_organizations": True, @@ -52,7 +48,6 @@ def setUp(self): "libraries_v2_enabled": False, "taxonomies_enabled": True, "taxonomy_list_mfe_url": 'http://course-authoring-mfe/taxonomies', - "optimization_enabled": False, "request_course_creator_url": "/request_course_creator", "rerun_creator_status": True, "show_new_library_button": True, @@ -84,6 +79,17 @@ def test_home_page_studio_with_meilisearch_enabled(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.data) + @override_settings(ORGANIZATIONS_AUTOCREATE=False) + def test_home_page_studio_with_org_autocreate_disabled(self): + """Check response content when Organization autocreate is disabled""" + response = self.client.get(self.url) + + expected_response = self.expected_response + expected_response["allow_to_create_new_org"] = False + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + def test_taxonomy_list_link(self): response = self.client.get(self.url) self.assertTrue(response.data['taxonomies_enabled']) @@ -242,27 +248,6 @@ def test_home_page_response_no_courses_non_staff(self, filter_key, filter_value) self.assertEqual(len(response.data["courses"]), 0) self.assertEqual(response.status_code, status.HTTP_200_OK) - @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) - def test_org_query_if_passed(self): - """Test home page when org filter passed as a query param""" - foo_course = self.store.make_course_key('foo-org', 'bar-number', 'baz-run') - test_course = CourseFactory.create( - org=foo_course.org, - number=foo_course.course, - run=foo_course.run - ) - CourseOverviewFactory.create(id=test_course.id, org='foo-org') - response = self.client.get(self.url, {"org": "foo-org"}) - self.assertEqual(len(response.data['courses']), 1) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) - def test_org_query_if_empty(self): - """Test home page with an empty org query param""" - response = self.client.get(self.url) - self.assertEqual(len(response.data['courses']), 0) - self.assertEqual(response.status_code, status.HTTP_200_OK) - @ddt.ddt class HomePageLibrariesViewTest(LibraryTestCase): diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py index e773e7f213c6..e899019b4f17 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py @@ -10,12 +10,10 @@ from django.conf import settings from django.test import override_settings from django.urls import reverse -from edx_toggles.toggles.testutils import override_waffle_switch from rest_framework import status from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.utils import reverse_course_url -from cms.djangoapps.contentstore.views.course import ENABLE_GLOBAL_STAFF_OPTIMIZATION from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory FEATURES_WITH_HOME_PAGE_COURSE_V2_API = settings.FEATURES.copy() @@ -104,30 +102,6 @@ def test_home_page_response(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.data) - @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) - def test_org_query_if_passed(self): - """Get list of courses when org filter passed as a query param. - - Expected result: - - A list of courses available to the logged in user for the specified org. - """ - response = self.client.get(self.api_v2_url, {"org": "demo-org"}) - - self.assertEqual(len(response.data['results']['courses']), 1) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) - def test_org_query_if_empty(self): - """Get home page with an empty org query param. - - Expected result: - - An empty list of courses available to the logged in user. - """ - response = self.client.get(self.api_v2_url) - - self.assertEqual(len(response.data['results']['courses']), 0) - self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_active_only_query_if_passed(self): """Get list of active courses only. diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 7023bcaefaf7..a220b8d91399 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1595,7 +1595,6 @@ def get_course_context(request): from cms.djangoapps.contentstore.views.course import ( get_courses_accessible_to_user, _process_courses_list, - ENABLE_GLOBAL_STAFF_OPTIMIZATION, ) def format_in_process_course_view(uca): @@ -1619,10 +1618,7 @@ def format_in_process_course_view(uca): ) if uca.state == CourseRerunUIStateManager.State.FAILED else '' } - optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() - - org = request.GET.get('org', '') if optimization_enabled else None - courses_iter, in_process_course_actions = get_courses_accessible_to_user(request, org) + courses_iter, in_process_course_actions = get_courses_accessible_to_user(request) split_archived = settings.FEATURES.get('ENABLE_SEPARATE_ARCHIVED_COURSES', False) active_courses, archived_courses = _process_courses_list(courses_iter, in_process_course_actions, split_archived) in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions] @@ -1637,7 +1633,6 @@ def get_course_context_v2(request): # 'cms.djangoapps.contentstore.utils' (most likely due to a circular import) from cms.djangoapps.contentstore.views.course import ( get_courses_accessible_to_user, - ENABLE_GLOBAL_STAFF_OPTIMIZATION, ) def format_in_process_course_view(uca): @@ -1664,10 +1659,7 @@ def format_in_process_course_view(uca): ) if uca.state == CourseRerunUIStateManager.State.FAILED else '' } - optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() - - org = request.GET.get('org', '') if optimization_enabled else None - courses_iter, in_process_course_actions = get_courses_accessible_to_user(request, org) + courses_iter, in_process_course_actions = get_courses_accessible_to_user(request) in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions] return courses_iter, in_process_course_actions @@ -1685,7 +1677,6 @@ def get_home_context(request, no_course=False): _accessible_libraries_iter, _get_course_creator_status, _format_library_for_view, - ENABLE_GLOBAL_STAFF_OPTIMIZATION, ) from cms.djangoapps.contentstore.views.library import ( user_can_view_create_library_button, @@ -1698,8 +1689,6 @@ def get_home_context(request, no_course=False): archived_courses = [] in_process_course_actions = [] - optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() - user = request.user libraries = [] @@ -1728,7 +1717,6 @@ def get_home_context(request, no_course=False): 'rerun_creator_status': GlobalStaff().has_user(user), 'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False), 'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True), - 'optimization_enabled': optimization_enabled, 'active_tab': 'courses', 'allowed_organizations': get_allowed_organizations(user), 'allowed_organizations_for_libraries': get_allowed_organizations_for_libraries(user), diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 244804c3062b..064cb1ad25e0 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -23,7 +23,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_GET, require_http_methods from edx_django_utils.monitoring import function_trace -from edx_toggles.toggles import WaffleSwitch from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import BlockUsageLocator @@ -138,11 +137,6 @@ 'group_configurations_list_handler', 'group_configurations_detail_handler', 'get_course_and_check_access'] -WAFFLE_NAMESPACE = 'studio_home' -ENABLE_GLOBAL_STAFF_OPTIMIZATION = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation - f'{WAFFLE_NAMESPACE}.enable_global_staff_optimization', __name__ -) - class AccessListFallback(Exception): """ @@ -394,15 +388,12 @@ def get_in_process_course_actions(request): ] -def _accessible_courses_summary_iter(request, org=None): +def _accessible_courses_summary_iter(request): """ List all courses available to the logged in user by iterating through all the courses Arguments: request: the request object - org (string): if not None, this value will limit the courses returned. An empty - string will result in no courses, and otherwise only courses with the - specified org will be returned. The default value is None. """ def course_filter(course_summary): """ @@ -416,9 +407,7 @@ def course_filter(course_summary): enable_home_page_api_v2 = settings.FEATURES["ENABLE_HOME_PAGE_COURSE_API_V2"] - if org is not None: - courses_summary = [] if org == '' else CourseOverview.get_all_courses(orgs=[org]) - elif enable_home_page_api_v2: + if enable_home_page_api_v2: # If the new home page API is enabled, we should use the Django ORM to filter and order the courses courses_summary = CourseOverview.get_all_courses() else: @@ -765,21 +754,17 @@ def course_index(request, course_key): @function_trace('get_courses_accessible_to_user') -def get_courses_accessible_to_user(request, org=None): +def get_courses_accessible_to_user(request): """ Try to get all courses by first reversing django groups and fallback to old method if it fails Note: overhead of pymongo reads will increase if getting courses from django groups fails Arguments: request: the request object - org (string): for global staff users ONLY, this value will be used to limit - the courses returned. A value of None will have no effect (all courses - returned), an empty string will result in no courses, and otherwise only courses with the - specified org will be returned. The default value is None. """ if GlobalStaff().has_user(request.user): # user has global access so no need to get courses from django groups - courses, in_process_course_actions = _accessible_courses_summary_iter(request, org) + courses, in_process_course_actions = _accessible_courses_summary_iter(request) else: try: courses, in_process_course_actions = _accessible_courses_list_from_groups(request) diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index 1190eb239518..cfbbcac5cde5 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -265,9 +265,10 @@ def test_get_container_nested_container_fragment(self): html, # The instance of the wrapper class will have an auto-generated ID. Allow any # characters after wrapper. - '"/container/{}" class="action-button">\\s*View'.format( - re.escape(str(wrapper_usage_key)) - ), + ( + '"/container/{}" class="action-button xblock-view-action-button">' + '\\s*View' + ).format(re.escape(str(wrapper_usage_key))), ) @patch("cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers.get_object_tag_counts") diff --git a/cms/static/js/views/container.js b/cms/static/js/views/container.js index 9370dfdc29d5..7bf3372c6148 100644 --- a/cms/static/js/views/container.js +++ b/cms/static/js/views/container.js @@ -70,6 +70,17 @@ function($, _, XBlockView, ModuleUtils, gettext, StringUtils, NotificationView) newParent = undefined; }, update: function(event, ui) { + try { + window.parent.postMessage( + { + type: 'refreshPositions', + message: 'Refresh positions of all xblocks', + payload: {} + }, document.referrer + ); + } catch (e) { + console.error(e); + } // When dragging from one ol to another, this method // will be called twice (once for each list). ui.sender will // be null if the change is related to the list the element diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index fb8fd2482d4e..7f5e2c257e22 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -39,6 +39,7 @@ function($, _, Backbone, gettext, BasePage, 'click .manage-tags-button': 'openManageTags', 'change .header-library-checkbox': 'toggleLibraryComponent', 'click .collapse-button': 'collapseXBlock', + 'click .xblock-view-action-button': 'viewXBlockContent', }, options: { @@ -391,12 +392,16 @@ function($, _, Backbone, gettext, BasePage, editXBlock: function(event, options) { event.preventDefault(); + const isAccessButton = event.currentTarget.className === 'access-button'; + const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions'); + const usageId = encodeURI(primaryHeader.attr('data-usage-id')); try { - if (this.options.isIframeEmbed) { - window.parent.postMessage( + if (this.options.isIframeEmbed && isAccessButton) { + return window.parent.postMessage( { - type: 'editXBlock', - payload: {} + type: 'manageXBlockAccess', + message: 'Open the manage access modal', + payload: { usageId } }, document.referrer ); } @@ -417,8 +422,26 @@ function($, _, Backbone, gettext, BasePage, || (useNewProblemEditor === 'True' && blockType === 'problem') ) { var destinationUrl = primaryHeader.attr('authoring_MFE_base_url') - + '/' + blockType - + '/' + encodeURI(primaryHeader.attr('data-usage-id')); + + '/' + blockType + + '/' + encodeURI(primaryHeader.attr('data-usage-id')); + + try { + if (this.options.isIframeEmbed) { + return window.parent.postMessage( + { + type: 'newXBlockEditor', + message: 'Open the new XBlock editor', + payload: { + blockType, + usageId: encodeURI(primaryHeader.attr('data-usage-id')), + } + }, document.referrer + ); + } + } catch (e) { + console.error(e); + } + var upstreamRef = primaryHeader.attr('data-upstream-ref'); if(upstreamRef) { destinationUrl += '?upstreamLibRef=' + upstreamRef; @@ -548,6 +571,65 @@ function($, _, Backbone, gettext, BasePage, // Code in 'base.js' normally handles toggling these dropdowns but since this one is // not present yet during the domReady event, we have to handle displaying it ourselves. subMenu.classList.toggle('is-shown'); + + if (!subMenu.classList.contains('is-shown') && this.options.isIframeEmbed) { + try { + window.parent.postMessage( + { + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { courseXBlockDropdownHeight: 0 } + }, document.referrer + ); + } catch (error) { + console.error(error); + } + } + + // Calculate the viewport height and the dropdown menu height. + // Check if the dropdown would overflow beyond the iframe height based on the user's click position. + // If the dropdown overflows, adjust its position to display above the click point. + const courseUnitXBlockIframeHeight = window.innerHeight; + const courseXBlockDropdownHeight = subMenu.offsetHeight; + const clickYPosition = event.clientY; + + if (courseUnitXBlockIframeHeight < courseXBlockDropdownHeight) { + // If the dropdown menu is taller than the iframe, adjust the height of the dropdown menu. + try { + window.parent.postMessage( + { + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { courseXBlockDropdownHeight }, + }, document.referrer + ); + } catch (error) { + console.error(error); + } + } else if ((courseXBlockDropdownHeight + clickYPosition) > courseUnitXBlockIframeHeight) { + if (courseXBlockDropdownHeight > courseUnitXBlockIframeHeight / 2) { + // If the dropdown menu is taller than half the iframe, send a message to adjust its height. + try { + window.parent.postMessage( + { + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { + courseXBlockDropdownHeight: courseXBlockDropdownHeight / 2, + }, + }, document.referrer + ); + } catch (error) { + console.error(error); + } + } else { + // Move the dropdown menu upward to prevent it from overflowing out of the viewport. + if (this.options.isIframeEmbed) { + subMenu.style.top = `-${courseXBlockDropdownHeight}px`; + } + } + } + // if propagation is not stopped, the event will bubble up to the // body element, which will close the dropdown. event.stopPropagation(); @@ -588,12 +670,15 @@ function($, _, Backbone, gettext, BasePage, copyXBlock: function(event) { event.preventDefault(); + const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions'); + const usageId = encodeURI(primaryHeader.attr('data-usage-id')); try { if (this.options.isIframeEmbed) { - window.parent.postMessage( + return window.parent.postMessage( { type: 'copyXBlock', - payload: {} + message: 'Copy the XBlock', + payload: { usageId } }, document.referrer ); } @@ -645,12 +730,16 @@ function($, _, Backbone, gettext, BasePage, duplicateXBlock: function(event) { event.preventDefault(); + const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions'); + const blockType = primaryHeader.attr('data-block-type'); + const usageId = encodeURI(primaryHeader.attr('data-usage-id')); try { if (this.options.isIframeEmbed) { - window.parent.postMessage( + return window.parent.postMessage( { type: 'duplicateXBlock', - payload: {} + message: 'Duplicate the XBlock', + payload: { blockType, usageId } }, document.referrer ); } @@ -702,12 +791,15 @@ function($, _, Backbone, gettext, BasePage, deleteXBlock: function(event) { event.preventDefault(); + const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions'); + const usageId = encodeURI(primaryHeader.attr('data-usage-id')); try { if (this.options.isIframeEmbed) { - window.parent.postMessage( + return window.parent.postMessage( { type: 'deleteXBlock', - payload: {} + message: 'Delete the XBlock', + payload: { usageId } }, document.referrer ); } @@ -717,10 +809,6 @@ function($, _, Backbone, gettext, BasePage, this.deleteComponent(this.findXBlockElement(event.target)); }, - createPlaceholderElement: function() { - return $('
', {class: 'studio-xblock-wrapper'}); - }, - createComponent: function(template, target) { // A placeholder element is created in the correct location for the new xblock // and then onNewXBlock will replace it with a rendering of the xblock. Note that @@ -814,6 +902,26 @@ function($, _, Backbone, gettext, BasePage, } }, + viewXBlockContent: function(event) { + try { + if (this.options.isIframeEmbed) { + event.preventDefault(); + var usageId = event.currentTarget.href.split('/').pop() || ''; + window.parent.postMessage( + { + type: 'handleViewXBlockContent', + payload: { + usageId: usageId, + }, + }, document.referrer + ); + return true; + } + } catch (e) { + console.error(e); + } + }, + toggleSaveButton: function() { var $saveButton = $('.nav-actions .save-button'); if (JSON.stringify(this.selectedLibraryComponents.sort()) === JSON.stringify(this.storedSelectedLibraryComponents.sort())) { @@ -868,12 +976,13 @@ function($, _, Backbone, gettext, BasePage, || (useNewProblemEditor === 'True' && blockType.includes('problem'))) ){ var destinationUrl; - if (useVideoGalleryFlow === "True" && blockType.includes("video")) { + if (useVideoGalleryFlow === 'True' && blockType.includes('video')) { destinationUrl = this.$('.xblock-header-primary').attr("authoring_MFE_base_url") + '/course-videos/' + encodeURI(data.locator); } else { destinationUrl = this.$('.xblock-header-primary').attr("authoring_MFE_base_url") + '/' + blockType[1] + '/' + encodeURI(data.locator); } + window.location.href = destinationUrl; return; } diff --git a/cms/static/sass/course-unit-mfe-iframe-bundle.scss b/cms/static/sass/course-unit-mfe-iframe-bundle.scss index a71882f1c355..bc0c3901b147 100644 --- a/cms/static/sass/course-unit-mfe-iframe-bundle.scss +++ b/cms/static/sass/course-unit-mfe-iframe-bundle.scss @@ -1,14 +1,22 @@ @import 'cms/theme/variables-v1'; @import 'elements/course-unit-mfe-iframe'; +body { + min-width: 800px; +} + .wrapper { + .inner-wrapper { + max-width: 100%; + } + .wrapper-xblock { background-color: $transparent; border-radius: 6px; border: none; &:hover { - border-color: none; + border-color: transparent; } .xblock-header-primary { @@ -23,6 +31,54 @@ } } + .xblock-header-secondary { + border-radius: 0 0 4px 4px; + + .actions-list .action-item .action-button { + border-radius: 4px; + + &:hover { + background-color: $primary; + color: $white; + } + } + } + + &.level-page .xblock-message { + padding: ($baseline * .75) ($baseline * 1.2); + border-radius: 0 0 4px 4px; + + &.information { + color: $text-color; + background-color: $xblock-message-info-bg; + border-color: $xblock-message-info-border-color; + } + + &.validation.has-warnings { + color: $black; + background-color: $xblock-message-warning-bg; + border-color: $xblock-message-warning-border-color; + border-top-width: 1px; + + .icon { + color: $xblock-message-warning-border-color; + } + } + + a { + color: $primary; + } + } + + .xblock-author_view-library_content > .wrapper-xblock-message .xblock-message { + font-size: 16px; + line-height: 22px; + border-radius: 4px; + padding: ($baseline * 1.2); + box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); + margin-bottom: ($baseline * 1.4); + } + &.level-element { box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15); margin: 0 0 ($baseline * 1.4) 0; @@ -40,27 +96,34 @@ border-bottom-right-radius: 6px; } - .wrapper-xblock .header-actions .actions-list .action-item .action-button { - @extend %button-styles; + .wrapper-xblock .header-actions .actions-list { + .action-actions-menu:last-of-type .nav-sub { + right: 120px; + } - color: $primary; + .action-item .action-button { + @extend %button-styles; - .fa-ellipsis-v { - font-size: $base-font-size; - } + color: $primary; - &:hover { - background-color: $primary; + .fa-ellipsis-v { + font-size: $base-font-size; + } + + &:hover { + background-color: $primary; + color: $white; + border-color: $transparent; color: $white; - border-color: $transparent; } - &:focus { - outline: 2px $transparent; - background-color: $transparent; - box-shadow: inset 0 0 0 2px $primary; - color: $primary; - border-color: $transparent; + &:focus { + outline: 2px $transparent; + background-color: $transparent; + box-shadow: inset 0 0 0 2px $primary; + color: $primary; + border-color: $transparent; + } } } @@ -327,6 +390,7 @@ .wrapper-content.wrapper { padding: $baseline / 4; + background-color: #f8f7f6; } .btn-default.action-edit.title-edit-button { @@ -629,7 +693,7 @@ select { } } -.xblock-header-primary { +.xblock-header:not(.xblock-header-library_content, .xblock-header-split_test) .xblock-header-primary { position: relative; &::before { @@ -656,3 +720,7 @@ select { .wrapper-comp-setting.metadata-list-enum .action.setting-clear.active { margin-top: 0; } + +.wrapper-xblock .xblock-header-primary .header-actions .wrapper-nav-sub { + z-index: $zindex-dropdown; +} diff --git a/cms/static/sass/partials/cms/theme/_variables-v1.scss b/cms/static/sass/partials/cms/theme/_variables-v1.scss index a008210b25b2..0b3fe6b6e49b 100644 --- a/cms/static/sass/partials/cms/theme/_variables-v1.scss +++ b/cms/static/sass/partials/cms/theme/_variables-v1.scss @@ -313,3 +313,10 @@ $light-background-color: #e1dddb !default; $border-color: #707070 !default; $base-font-size: 18px !default; $dark: #212529; + +$zindex-dropdown: 100; + +$xblock-message-info-bg: #eff8fa; +$xblock-message-info-border-color: #9cd2e6; +$xblock-message-warning-bg: #fffdf0; +$xblock-message-warning-border-color: #fff6bf; diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html index 41555410236a..8f4090588613 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -229,7 +229,7 @@
${_('This block contains multiple components.')}