From 838977a8f3981be491a7fa2441724e4e6e92d895 Mon Sep 17 00:00:00 2001 From: Muhammad Arslan Date: Wed, 1 Jan 2025 20:18:10 +0500 Subject: [PATCH 1/6] feat!: Remove DEPR waffle switch: ENABLE_GLOBAL_STAFF_OPTIMIZATION --- .../rest_api/v1/serializers/home.py | 1 - .../contentstore/rest_api/v1/views/home.py | 1 - .../rest_api/v1/views/tests/test_home.py | 23 ----------------- .../rest_api/v2/views/tests/test_home.py | 25 ------------------- cms/djangoapps/contentstore/utils.py | 16 ++---------- cms/djangoapps/contentstore/views/course.py | 20 +++------------ 6 files changed, 6 insertions(+), 80 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index fa2a651f8a28..36166e42a5a6 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -62,7 +62,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..2d5360d38d6c 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -62,7 +62,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, 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..e78558bba330 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 @@ -15,7 +15,6 @@ 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 @@ -52,7 +51,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, @@ -242,27 +240,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..bf3332e1e3cd 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 @@ -15,7 +15,6 @@ 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 +103,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..e05ac894d6d8 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -139,9 +139,6 @@ '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 +391,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 +410,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 +757,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) From f6f72290b79de36f3c045355a5acc725c8fef3a9 Mon Sep 17 00:00:00 2001 From: Muhammad Arslan Date: Wed, 1 Jan 2025 21:14:21 +0500 Subject: [PATCH 2/6] build: Remove unused imports --- .../contentstore/rest_api/v1/views/tests/test_home.py | 4 ---- .../contentstore/rest_api/v2/views/tests/test_home.py | 1 - cms/djangoapps/contentstore/views/course.py | 1 - 3 files changed, 6 deletions(-) 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 e78558bba330..31cb606b5d4b 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,15 +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 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() 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 bf3332e1e3cd..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,7 +10,6 @@ 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 diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index e05ac894d6d8..3b8cb7659e0e 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 From 343d5216193ed4df539d6bd14fa59cad82821963 Mon Sep 17 00:00:00 2001 From: Muhammad Arslan Date: Thu, 2 Jan 2025 22:42:19 +0500 Subject: [PATCH 3/6] chore: Remove unused variable and rebased --- cms/djangoapps/contentstore/views/course.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 3b8cb7659e0e..064cb1ad25e0 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -137,8 +137,6 @@ 'group_configurations_list_handler', 'group_configurations_detail_handler', 'get_course_and_check_access'] -WAFFLE_NAMESPACE = 'studio_home' - class AccessListFallback(Exception): """ From 5f302a3ae70f1fbc001536444592dc9b5fc831fb Mon Sep 17 00:00:00 2001 From: Muhammad Anas Date: Fri, 10 Jan 2025 18:53:04 +0500 Subject: [PATCH 4/6] fix: date label for instructor paced courses --- lms/djangoapps/courseware/date_summary.py | 2 +- .../courseware/tests/test_date_summary.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index 4123da5f38b9..e6bd5ef70597 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -268,7 +268,7 @@ def date_type(self): @property def title(self): enrollment = CourseEnrollment.get_enrollment(self.user, self.course_id) - if enrollment and self.course.end and enrollment.created > self.course.end: + if self.course.self_paced and enrollment and self.course.start and enrollment.created > self.course.start: return gettext_lazy('Enrollment Date') return gettext_lazy('Course starts') diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 27e7f1a3c226..e487af8a1f70 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -400,6 +400,37 @@ def test_course_start_date(self): block = CourseStartDate(course, user) assert block.date == course.start + @ddt.data( + # Instructor-paced course: Use course start date + (False, datetime(2025, 1, 10, tzinfo=utc), datetime(2025, 1, 12, tzinfo=utc), + datetime(2025, 1, 10, tzinfo=utc), 'Course starts'), + + # Self-paced course: Enrollment created later than course start + (True, datetime(2025, 1, 10, tzinfo=utc), datetime(2025, 1, 12), datetime(2025, 1, 12, tzinfo=utc), + 'Enrollment Date'), + + # Self-paced course: Enrollment created earlier than course start + (True, datetime(2025, 1, 10, tzinfo=utc), datetime(2025, 1, 8), datetime(2025, 1, 10, tzinfo=utc), + 'Course starts'), + + # Self-paced course: No enrollment + (True, datetime(2025, 1, 10, tzinfo=utc), None, datetime(2025, 1, 10, tzinfo=utc), 'Course starts'), + ) + @ddt.unpack + def test_course_start_date_label(self, self_paced, course_start, enrollment_created, expected_date, expected_title): + """ + Test the CourseStartDate class has correct label for course start date + """ + course = CourseFactory(self_paced=self_paced, start=course_start) + user = create_user() + if enrollment_created: + enrollment = CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) + enrollment.created = enrollment_created + enrollment.save() + date_summary = CourseStartDate(user=user, course=course) + self.assertEqual(date_summary.date, expected_date) + self.assertEqual(str(date_summary.title), expected_title) + ## Tests Course End Date Block def test_course_end_date_for_certificate_eligible_mode(self): course = create_course_run(days_till_start=-1) From 511b440045d650666ae1f3bf0ba5bf47d29b376b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:44:53 +0500 Subject: [PATCH 5/6] feat: Upgrade Python dependency edx-enterprise (#36105) fix: Log all learner transmission records. Commit generated by workflow `openedx/edx-platform/.github/workflows/upgrade-one-python-dependency.yml@refs/heads/master` Co-authored-by: zamanafzal <11922730+zamanafzal@users.noreply.github.com> Co-authored-by: Zaman Afzal --- requirements/constraints.txt | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 79e55d633c7d..865d224beab7 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -80,7 +80,7 @@ django-storages<1.14.4 # The team that owns this package will manually bump this package rather than having it pulled in automatically. # This is to allow them to better control its deployment and to do it in a process that works better # for them. -edx-enterprise==5.5.2 +edx-enterprise==5.6.1 # Date: 2024-05-09 # This has to be constrained as well because newer versions of edx-i18n-tools need the diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index a6f79ba3cd7d..86431ca50e3e 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -468,7 +468,7 @@ edx-drf-extensions==10.5.0 # edx-when # edxval # openedx-learning -edx-enterprise==5.5.2 +edx-enterprise==5.6.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index e353c37e9541..e328b6dcb1f2 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -747,7 +747,7 @@ edx-drf-extensions==10.5.0 # edx-when # edxval # openedx-learning -edx-enterprise==5.5.2 +edx-enterprise==5.6.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 68cebb2d3e6d..97c9de68dfd9 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -555,7 +555,7 @@ edx-drf-extensions==10.5.0 # edx-when # edxval # openedx-learning -edx-enterprise==5.5.2 +edx-enterprise==5.6.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index e27fb1195b95..5e99738b3cc8 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -576,7 +576,7 @@ edx-drf-extensions==10.5.0 # edx-when # edxval # openedx-learning -edx-enterprise==5.5.2 +edx-enterprise==5.6.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt From 755bf9809127988c0278fe5aef2b7c80aa34b742 Mon Sep 17 00:00:00 2001 From: Peter Kulko <93188219+PKulkoRaccoonGang@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:56:51 +0200 Subject: [PATCH 6/6] feat: [FC-0070] Listen to xBlock interaction events (#35694) This is part of the effort to support the new Studio Unit Page embedded in the authoring MFE. It introduces several changes to improve the handling of XBlock events and the user interface in the CMS. The most important changes include adding event listeners for message handling, refining the postMessage logic, and updating the CSS for better visual consistency. --- cms/static/js/views/container.js | 11 ++ cms/static/js/views/pages/container.js | 118 ++++++++++++++++-- .../sass/course-unit-mfe-iframe-bundle.scss | 6 + .../partials/cms/theme/_variables-v1.scss | 2 + 4 files changed, 124 insertions(+), 13 deletions(-) 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..304e3bc92ddf 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -391,12 +391,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 +421,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 +570,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 +669,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 +729,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 +790,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 ); } @@ -868,12 +959,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..7176300da114 100644 --- a/cms/static/sass/course-unit-mfe-iframe-bundle.scss +++ b/cms/static/sass/course-unit-mfe-iframe-bundle.scss @@ -53,6 +53,7 @@ background-color: $primary; color: $white; border-color: $transparent; + color: $white; } &:focus { @@ -327,6 +328,7 @@ .wrapper-content.wrapper { padding: $baseline / 4; + background-color: #f8f7f6; } .btn-default.action-edit.title-edit-button { @@ -656,3 +658,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..c48d78ba8481 100644 --- a/cms/static/sass/partials/cms/theme/_variables-v1.scss +++ b/cms/static/sass/partials/cms/theme/_variables-v1.scss @@ -313,3 +313,5 @@ $light-background-color: #e1dddb !default; $border-color: #707070 !default; $base-font-size: 18px !default; $dark: #212529; + +$zindex-dropdown: 100;