From 9d0424cd3f57a95b5c46057663934b2064d25535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Tue, 21 Nov 2023 15:12:17 -0500 Subject: [PATCH] feat: Display tags on the Unit page in Studio (feature-flagged) - Take 2 (#33761) --- cms/djangoapps/contentstore/utils.py | 8 +- cms/djangoapps/contentstore/views/block.py | 9 +- .../contentstore/views/component.py | 101 ++++++++++- cms/static/js/models/xblock_info.js | 6 +- cms/static/js/views/course_outline.js | 38 +--- cms/static/js/views/pages/container.js | 23 ++- .../js/views/pages/container_subviews.js | 171 +++++++++++++++++- .../js/views/utils/tagging_drawer_utils.js | 55 ++++++ cms/static/sass/elements/_controls.scss | 28 +++ .../partials/cms/theme/_variables-v1.scss | 2 +- cms/static/sass/views/_container.scss | 71 ++++++++ cms/templates/container.html | 8 +- cms/templates/js/tag-list.underscore | 32 ++++ cms/templates/studio_xblock_wrapper.html | 12 +- 14 files changed, 511 insertions(+), 53 deletions(-) create mode 100644 cms/static/js/views/utils/tagging_drawer_utils.js create mode 100644 cms/templates/js/tag-list.underscore diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index a4c69e55b7ea..190b7dca3473 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,7 +1,7 @@ """ Common utility functions useful throughout the contentstore """ - +from __future__ import annotations import logging from contextlib import contextmanager from datetime import datetime @@ -395,7 +395,7 @@ def get_taxonomy_list_url(): return taxonomy_list_url -def get_taxonomy_tags_widget_url(course_locator) -> str: +def get_taxonomy_tags_widget_url(course_locator=None) -> str | None: """ Gets course authoring microfrontend URL for taxonomy tags drawer widget view. @@ -404,7 +404,9 @@ def get_taxonomy_tags_widget_url(course_locator) -> str: taxonomy_tags_widget_url = None # Uses the same waffle flag as taxonomy list page if use_tagging_taxonomy_list_page(): - mfe_base_url = get_course_authoring_url(course_locator) + mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL + if course_locator: + mfe_base_url = get_course_authoring_url(course_locator) if mfe_base_url: taxonomy_tags_widget_url = f'{mfe_base_url}/tagging/components/widget/' return taxonomy_tags_widget_url diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index ecaa6928c0b9..f27b3f955046 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -68,7 +68,8 @@ get_visibility_partition_info, has_children_visible_to_specific_partition_groups, is_currently_visible_to_students, - is_self_paced + is_self_paced, + get_taxonomy_tags_widget_url, ) from .helpers import ( create_xblock, @@ -1141,7 +1142,7 @@ def _get_gating_info(course, xblock): @pluggable_override('OVERRIDE_CREATE_XBLOCK_INFO') def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, # lint-amnesty, pylint: disable=too-many-statements course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None, - user=None, course=None, is_concise=False): + user=None, course=None, is_concise=False, tags=None): """ Creates the information needed for client-side XBlockInfo. @@ -1362,6 +1363,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F xblock_info['ancestor_has_staff_lock'] = ancestor_has_staff_lock(xblock, parent_xblock) else: xblock_info['ancestor_has_staff_lock'] = False + if tags is not None: + xblock_info["tags"] = tags + if use_tagging_taxonomy_list_page(): + xblock_info["taxonomy_tags_widget_url"] = get_taxonomy_tags_widget_url() if course_outline: if xblock_info['has_explicit_staff_lock']: diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index b7dd09d5c7cb..6be62ed2b1f4 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -24,9 +24,13 @@ from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag -from cms.djangoapps.contentstore.toggles import use_new_problem_editor +from cms.djangoapps.contentstore.toggles import ( + use_new_problem_editor, + use_tagging_taxonomy_list_page, +) from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration +from openedx.core.djangoapps.content_tagging.api import get_object_tags from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order @@ -55,7 +59,7 @@ "editor-mode-button", "upload-dialog", "add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", "add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem", - "xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history", + "xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history", "tag-list", "unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button", "edit-title-button", ] @@ -103,7 +107,7 @@ def _load_mixed_class(category): @require_GET @login_required -def container_handler(request, usage_key_string): +def container_handler(request, usage_key_string): # pylint: disable=too-many-statements """ The restful handler for container xblock requests. @@ -170,9 +174,14 @@ def container_handler(request, usage_key_string): prev_url = quote_plus(prev_url) if prev_url else None next_url = quote_plus(next_url) if next_url else None + show_unit_tags = use_tagging_taxonomy_list_page() + unit_tags = None + if show_unit_tags and is_unit_page: + unit_tags = get_unit_tags(usage_key) + # Fetch the XBlock info for use by the container page. Note that it includes information # about the block's ancestors and siblings for use by the Unit Outline. - xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page) + xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags) if is_unit_page: add_container_page_publishing_info(xblock, xblock_info) @@ -205,7 +214,8 @@ def container_handler(request, usage_key_string): 'xblock_info': xblock_info, 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, - 'templates': CONTAINER_TEMPLATES + 'templates': CONTAINER_TEMPLATES, + 'show_unit_tags': show_unit_tags, }) else: return HttpResponseBadRequest("Only supports HTML requests") @@ -583,3 +593,84 @@ def component_handler(request, usage_key_string, handler, suffix=''): ) return webob_to_django_response(resp) + + +def get_unit_tags(usage_key): + """ + Get the tags of a Unit and build a json to be read by the UI + + Note: When migrating the `TagList` subview from `container_subview.js` to the course-authoring MFE, + this function can be simplified to use the REST API of openedx-learning, + which already provides this grouping + sorting logic. + """ + # Get content tags from content tagging API + content_tags = get_object_tags(usage_key) + + # Group content tags by taxonomy + taxonomy_dict = {} + for content_tag in content_tags: + taxonomy_id = content_tag.taxonomy_id + # When a taxonomy is deleted, the id here is None. + # In that case the tag is not shown in the UI. + if taxonomy_id: + if taxonomy_id not in taxonomy_dict: + taxonomy_dict[taxonomy_id] = [] + taxonomy_dict[taxonomy_id].append(content_tag) + + taxonomy_list = [] + total_count = 0 + + def handle_tag(tags, root_ids, tag, child_tag_id=None): + """ + Group each tag by parent to build a tree. + """ + tag_processed_before = tag.id in tags + if not tag_processed_before: + tags[tag.id] = { + 'id': tag.id, + 'value': tag.value, + 'children': [], + } + if child_tag_id: + # Add a child into the children list + tags[tag.id].get('children').append(tags[child_tag_id]) + if tag.parent_id is None: + if tag.id not in root_ids: + root_ids.append(tag.id) + elif not tag_processed_before: + # Group all the lineage of this tag. + # + # Skip this if the tag has been processed before, + # we don't need to process lineage again to avoid duplicates. + handle_tag(tags, root_ids, tag.parent, tag.id) + + # Build a tag tree for each taxonomy + for content_tag_list in taxonomy_dict.values(): + tags = {} + root_ids = [] + + for content_tag in content_tag_list: + # When a tag is deleted from the taxonomy, the `tag` here is None. + # In that case the tag is not shown in the UI. + if content_tag.tag: + handle_tag(tags, root_ids, content_tag.tag) + + taxonomy = content_tag_list[0].taxonomy + + if tags: + count = len(tags) + # Add the tree to the taxonomy list + taxonomy_list.append({ + 'id': taxonomy.id, + 'value': taxonomy.name, + 'tags': [tags[tag_id] for tag_id in root_ids], + 'count': count, + }) + total_count += count + + unit_tags = { + 'count': total_count, + 'taxonomies': taxonomy_list, + } + + return unit_tags diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 983edc4e5648..d2227218fad3 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -167,7 +167,11 @@ define( highlights_enabled: false, highlights_enabled_for_messaging: false, highlights_preview_only: true, - highlights_doc_url: '' + highlights_doc_url: '', + /** + * List of tags of the unit. This list is managed by the content_tagging module. + */ + tags: null, }, initialize: function() { diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index ed30c1b34ddc..61ce21423aa1 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -9,10 +9,11 @@ * - adding units will automatically redirect to the unit page rather than showing them inline */ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils', - 'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop'], + 'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop', + 'js/views/utils/tagging_drawer_utils',], function( $, _, XBlockOutlineView, ViewUtils, XBlockViewUtils, - XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger + XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, TaggingDrawerUtils ) { var CourseOutlineView = XBlockOutlineView.extend({ // takes XBlockOutlineInfo as a model @@ -215,41 +216,12 @@ function( } }, - closeManageTagsDrawer(drawer, drawerCover) { - $(drawerCover).css('display', 'none'); - $(drawer).empty(); - $(drawer).css('display', 'none'); - $('body').removeClass('drawer-open'); - }, - - openManageTagsDrawer(event) { - const drawer = document.querySelector("#manage-tags-drawer"); - const drawerCover = document.querySelector(".drawer-cover") + openManageTagsDrawer() { const article = document.querySelector('[data-taxonomy-tags-widget-url]'); const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url'); const contentId = this.model.get('id'); - // Add handler to close drawer when dark background is clicked - $(drawerCover).click(function() { - this.closeManageTagsDrawer(drawer, drawerCover); - }.bind(this)); - - // Add event listen to close drawer when close button is clicked from within the Iframe - window.addEventListener("message", function (event) { - if (event.data === 'closeManageTagsDrawer') { - this.closeManageTagsDrawer(drawer, drawerCover) - } - }.bind(this)); - - $(drawerCover).css('display', 'block'); - // xss-lint: disable=javascript-jquery-html - $(drawer).html( - `` - ); - $(drawer).css('display', 'block'); - - // Prevent background from being scrollable when drawer is open - $('body').addClass('drawer-open'); + TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId); }, addButtonActions: function(element) { diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 4ce217ff0d35..5a69482c34c8 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -6,10 +6,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page 'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock', 'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal', 'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor', - 'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils'], + 'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils', + 'js/views/utils/tagging_drawer_utils'], function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor, - ContainerSubviews, UnitOutlineView, XBlockUtils) { + ContainerSubviews, UnitOutlineView, XBlockUtils, TaggingDrawerUtils) { 'use strict'; var XBlockContainerPage = BasePage.extend({ // takes XBlockInfo as a model @@ -22,7 +23,8 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView 'click .move-button': 'showMoveXBlockModal', 'click .delete-button': 'deleteXBlock', 'click .show-actions-menu-button': 'showXBlockActionsMenu', - 'click .new-component-button': 'scrollToNewComponentButtons' + 'click .new-component-button': 'scrollToNewComponentButtons', + 'click .tags-button': 'openManageTags', }, options: { @@ -92,6 +94,12 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView }); this.viewLiveActions.render(); + this.tagListView = new ContainerSubviews.TagList({ + el: this.$('.unit-tags'), + model: this.model + }); + this.tagListView.render(); + this.unitOutlineView = new UnitOutlineView({ el: this.$('.wrapper-unit-overview'), model: this.model @@ -119,6 +127,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView xblockView = this.xblockView, loadingElement = this.$('.ui-loading'), unitLocationTree = this.$('.unit-location'), + unitTags = this.$('.unit-tags'), hiddenCss = 'is-hidden'; loadingElement.removeClass(hiddenCss); @@ -144,6 +153,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView // Refresh the views now that the xblock is visible self.onXBlockRefresh(xblockView); unitLocationTree.removeClass(hiddenCss); + unitTags.removeClass(hiddenCss); // Re-enable Backbone events for any updated DOM elements self.delegateEvents(); @@ -247,6 +257,13 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView this.duplicateComponent(this.findXBlockElement(event.target)); }, + openManageTags: function(event) { + const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url'); + const contentId = this.findXBlockElement(event.target).data('locator'); + + TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId); + }, + showMoveXBlockModal: function(event) { var xblockElement = this.findXBlockElement(event.target), parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'), diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index cdcbca619f02..74a012732a76 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -2,8 +2,9 @@ * Subviews (usually small side panels) for XBlockContainerPage. */ define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils', - 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils'], -function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils) { + 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils', + 'js/views/utils/tagging_drawer_utils'], +function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils, TaggingDrawerUtils) { 'use strict'; var disabledCss = 'is-disabled'; @@ -294,11 +295,175 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H } }); + /** + * TagList displays the tags of a unit. + */ + var TagList = BaseView.extend({ + // takes XBlockInfo as a model + + events: { + 'click .wrapper-tag-header': 'expandTagContainer', + 'click .tagging-label': 'expandContentTag', + 'click .manage-tag-button': 'openManageTagDrawer', + 'keydown .wrapper-tag-header': 'handleKeyDownOnHeader', + 'keydown .tagging-label': 'handleKeyDownOnContentTag', + 'keydown .manage-tag-button': 'handleKeyDownOnTagDrawer', + }, + + initialize: function() { + BaseView.prototype.initialize.call(this); + this.template = this.loadTemplate('tag-list'); + this.model.on('sync', this.onSync, this); + }, + + onSync: function(model) { + if (ViewUtils.hasChangedAttributes(model, ['tags'])) { + this.render(); + } + }, + + handleKeyDownOnHeader: function(event) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.expandTagContainer(); + } + }, + + handleKeyDownOnContentTag: function(event) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.expandContentTag(event); + } + }, + + handleKeyDownOnTagDrawer: function(event) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.openManageTagDrawer(); + } + }, + + expandTagContainer: function() { + var $content = this.$('.wrapper-tags .wrapper-tag-content'), + $header = this.$('.wrapper-tags .wrapper-tag-header'), + $icon = this.$('.wrapper-tags .wrapper-tag-header .icon'); + + if ($content.hasClass('is-hidden')) { + $content.removeClass('is-hidden'); + $icon.addClass('fa-caret-up'); + $icon.removeClass('fa-caret-down'); + $header.attr('aria-expanded', 'true'); + } else { + $content.addClass('is-hidden'); + $icon.removeClass('fa-caret-up'); + $icon.addClass('fa-caret-down'); + $header.attr('aria-expanded', 'false'); + } + }, + + expandContentTag: function(event) { + var contentId = event.target.id, + $content = this.$(`.wrapper-tags .content-tags-${contentId}`), + $header = this.$(`.wrapper-tags .tagging-label-${contentId}`), + $icon = this.$(`.wrapper-tags .tagging-label-${contentId} .icon`); + + if ($content.hasClass('is-hidden')) { + $content.removeClass('is-hidden'); + $icon.addClass('fa-caret-up'); + $icon.removeClass('fa-caret-down'); + $header.attr('aria-expanded', 'true'); + } else { + $content.addClass('is-hidden'); + $icon.removeClass('fa-caret-up'); + $icon.addClass('fa-caret-down'); + $header.attr('aria-expanded', 'false'); + } + }, + + renderTagElements: function(tags, depth, parentId) { + const tagListElement = this; + tags.forEach(function(tag) { + const parentElement = document.querySelector(`.content-tags-${parentId}`); + var tagContentElement = document.createElement('div'), + tagValueElement = document.createElement('span'); + + // Element that contains the tag value and the arrow icon + tagContentElement.style.marginLeft = `${depth}em`; + tagContentElement.className = `tagging-label tagging-label-tag-${tag.id}`; + tagContentElement.id = `tag-${tag.id}`; + + // Element that contains the tag value + tagValueElement.textContent = tag.value; + tagValueElement.id = `tag-${tag.id}`; + tagValueElement.className = 'tagging-label-value'; + + tagContentElement.appendChild(tagValueElement); + parentElement.appendChild(tagContentElement); + + if (tag.children.length > 0) { + var tagIconElement = document.createElement('span'), + tagChildrenElement = document.createElement('div'); + + // Arrow icon + tagIconElement.className = 'icon fa fa-caret-down'; + tagIconElement.ariaHidden = 'true'; + tagIconElement.id = `tag-${tag.id}`; + + // Element that contains the children of this tag + tagChildrenElement.className = `content-tags-tag-${tag.id} is-hidden`; + + tagContentElement.tabIndex = 0; + tagContentElement.role = "button"; + tagContentElement.ariaExpanded = "false"; + tagContentElement.setAttribute('aria-controls', `content-tags-tag-${tag.id}`); + tagContentElement.appendChild(tagIconElement); + parentElement.appendChild(tagChildrenElement); + + // Render children + tagListElement.renderTagElements(tag.children, depth + 1, `tag-${tag.id}`); + } + }); + }, + + renderTags: function() { + if (this.model.get('tags') !== null) { + const taxonomies = this.model.get('tags').taxonomies; + const tagListElement = this; + taxonomies.forEach(function(taxonomy) { + tagListElement.renderTagElements(taxonomy.tags, 1, `tax-${taxonomy.id}`); + }); + } + }, + + openManageTagDrawer: function() { + const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url'); + const contentId = this.model.get('id'); + + TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId); + }, + + render: function() { + HtmlUtils.setHtml( + this.$el, + HtmlUtils.HTML( + this.template({ + tags: this.model.get('tags'), + }) + ) + ); + + this.renderTags(); + + return this; + } + }); + return { MessageView: MessageView, ViewLiveButtonController: ViewLiveButtonController, Publisher: Publisher, PublishHistory: PublishHistory, - ContainerAccess: ContainerAccess + ContainerAccess: ContainerAccess, + TagList: TagList }; }); // end define(); diff --git a/cms/static/js/views/utils/tagging_drawer_utils.js b/cms/static/js/views/utils/tagging_drawer_utils.js new file mode 100644 index 000000000000..227c19e9cd2a --- /dev/null +++ b/cms/static/js/views/utils/tagging_drawer_utils.js @@ -0,0 +1,55 @@ +/** + * Provides utilities to open and close the tagging drawer to manage tags. + * + * To use this drawer you need to add the following code into your template: + * + * ``` + *
+ *
+ * ``` + */ +define(['jquery'], +function($) { + 'use strict'; + + var closeDrawer, openDrawer; + + closeDrawer = function(drawer, drawerCover) { + $(drawerCover).css('display', 'none'); + $(drawer).empty(); + $(drawer).css('display', 'none'); + $('body').removeClass('drawer-open'); + }; + + openDrawer = function(taxonomyTagsWidgetUrl, contentId) { + const drawer = document.querySelector("#manage-tags-drawer"); + const drawerCover = document.querySelector(".drawer-cover"); + + // Add handler to close drawer when dark background is clicked + $(drawerCover).click(function() { + closeDrawer(drawer, drawerCover); + }.bind(this)); + + // Add event listen to close drawer when close button is clicked from within the Iframe + window.addEventListener("message", function (event) { + if (event.data === 'closeManageTagsDrawer') { + closeDrawer(drawer, drawerCover) + } + }.bind(this)); + + $(drawerCover).css('display', 'block'); + // xss-lint: disable=javascript-jquery-html + $(drawer).html( + `` + ); + $(drawer).css('display', 'block'); + + // Prevent background from being scrollable when drawer is open + $('body').addClass('drawer-open'); + }; + + return { + openDrawer: openDrawer, + closeDrawer: closeDrawer + }; +}); diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss index 8420961ab4ab..3a9c3af3db89 100644 --- a/cms/static/sass/elements/_controls.scss +++ b/cms/static/sass/elements/_controls.scss @@ -119,6 +119,34 @@ } } +// inverse primary button +%btn-primary-inverse { + @extend %ui-btn-primary; + + background: theme-color("inverse"); + border-color: theme-color("primary"); + color: theme-color("primary"); + + &:hover, + &:active { + background: theme-color("primary"); + border-color: $uxpl-blue-hover-active; + color: theme-color("inverse"); + } + + &.current, + &.active { + background: theme-color("primary"); + border-color: $uxpl-blue-hover-active; + color: theme-color("inverse"); + + &:hover, + &:active { + background: theme-color("primary"); + } + } +} + // +Secondary Button - Extends // ==================== // gray secondary button diff --git a/cms/static/sass/partials/cms/theme/_variables-v1.scss b/cms/static/sass/partials/cms/theme/_variables-v1.scss index 96777b34f27a..9dd001d455cc 100644 --- a/cms/static/sass/partials/cms/theme/_variables-v1.scss +++ b/cms/static/sass/partials/cms/theme/_variables-v1.scss @@ -28,7 +28,7 @@ $fg-column: $gw-column; $fg-gutter: $gw-gutter; $fg-max-columns: 12; $fg-max-width: 1280px; -$fg-min-width: 900px; +$fg-min-width: 995px; // +Fonts // ==================== diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index cf8824ca5110..2b55b00cb6f4 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -242,6 +242,77 @@ } } + .unit-tags { + .wrapper-tags { + margin-bottom: $baseline; + padding: ($baseline*0.75); + background-color: $white; + + .wrapper-tag-header { + display: flex; + justify-content: space-between; + + .tag-title { + font-weight: bold; + } + + .count-badge { + background-color: $gray-l5; + border-radius: 50%; + display: inline-block; + padding: 0px 8px; + } + } + + .wrapper-tag-header:focus { + border: 1px dotted gray; + } + + .action-primary { + @extend %btn-primary-inverse; + + width: 100%; + margin: 16px 2px 8px 2px; + } + + .wrapper-tag-content { + background-color: $white; + + .content-taxonomies { + display: flex; + flex-direction: column; + padding-top: 10px; + + .tagging-label { + display: flex; + padding: 4px 0px; + + .tagging-label-value { + display: inline-block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .tagging-label-count { + display: inline-block; + margin: 0 0.5em; + } + } + + .tagging-label:hover, + .tagging-label:focus { + color: $blue; + } + + .icon { + margin-left: 5px; + } + } + } + } + } + // versioning widget .unit-publish-history { .wrapper-last-publish { diff --git a/cms/templates/container.html b/cms/templates/container.html index ffafa8d96e26..e5a99b11a0c1 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -206,7 +206,10 @@
${_("Location ID")}
Tip: ${Text(_('To create a link to this unit from an HTML component in this course, enter "/jump_to_id/" as the URL value.'))}

- + + % if show_unit_tags: + + % endif % endif @@ -231,4 +234,7 @@
${_("Location ID")}
+ +
+
diff --git a/cms/templates/js/tag-list.underscore b/cms/templates/js/tag-list.underscore new file mode 100644 index 000000000000..a006eb111b5b --- /dev/null +++ b/cms/templates/js/tag-list.underscore @@ -0,0 +1,32 @@ +<% if (tags !== null) { %> +
+ + +
+<% } %> diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html index 81a08a4d0105..b85ad79fbe4c 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -7,12 +7,13 @@ from openedx.core.djangolib.js_utils import ( dump_js_escaped_json, js_escaped_string ) -from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor +from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, use_tagging_taxonomy_list_page %> <% use_new_editor_text = use_new_text_editor() use_new_editor_video = use_new_video_editor() use_new_editor_problem = use_new_problem_editor() +use_tagging = use_tagging_taxonomy_list_page() xblock_url = xblock_studio_url(xblock) show_inline = xblock.has_children and not xblock_url section_class = "level-nesting" if show_inline else "level-element" @@ -119,6 +120,15 @@ % endif + % if use_tagging: +
  • + + + ? + ${_("Manage tags")} + +
  • + % endif % endif % if can_add and not enable_copy_paste: