diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index ba227ecbec0e..8e3f97eee98e 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,6 +1,7 @@ """ Common utility functions useful throughout the contentstore """ +from __future__ import annotations import configparser import logging from collections import defaultdict @@ -442,7 +443,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. @@ -451,7 +452,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/component.py b/cms/djangoapps/contentstore/views/component.py index 3c31637d4c78..3cd3ffff90e2 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -25,10 +25,14 @@ 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_staging import api as content_staging_api +from openedx.core.djangoapps.content_tagging.api import get_content_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 from ..toggles import use_new_unit_page @@ -61,7 +65,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", ] @@ -178,9 +182,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) @@ -216,6 +225,7 @@ def container_handler(request, usage_key_string): 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'templates': CONTAINER_TEMPLATES, + 'show_unit_tags': show_unit_tags, # Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API. 'user_clipboard': user_clipboard, }) @@ -598,3 +608,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_content_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/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index dca25695a29d..a563c6b61da4 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -99,6 +99,7 @@ has_children_visible_to_specific_partition_groups, is_currently_visible_to_students, is_self_paced, + get_taxonomy_tags_widget_url, ) from .create_xblock import create_xblock @@ -1093,6 +1094,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements course=None, is_concise=False, summary_configuration=None, + tags=None, ): """ Creates the information needed for client-side XBlockInfo. @@ -1385,6 +1387,10 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements ) 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/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index d7d9ef278b01..49812a6c9d78 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -173,6 +173,10 @@ define( * True if summary configuration is enabled. */ summary_configuration_enabled: null, + /** + * 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 2faccde97e36..4b7107cc4ddc 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -11,10 +11,12 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'edx-ui-toolkit/js/utils/string-utils', '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', - 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',], + 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', + 'js/views/utils/tagging_drawer_utils',], function( $, _, XBlockOutlineView, StringUtils, ViewUtils, XBlockViewUtils, - XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView + XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView, + TaggingDrawerUtils ) { var CourseOutlineView = XBlockOutlineView.extend({ // takes XBlockOutlineInfo as a model @@ -458,41 +460,12 @@ function( event.stopPropagation(); }, - 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 8f296ebb6b58..71cbb9b26d5f 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -8,10 +8,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page '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', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', + 'js/views/utils/tagging_drawer_utils', ], function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor, - ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView) { + ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView, TaggingDrawerUtils) { 'use strict'; var XBlockContainerPage = BasePage.extend({ @@ -27,6 +28,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView 'click .show-actions-menu-button': 'showXBlockActionsMenu', 'click .new-component-button': 'scrollToNewComponentButtons', 'click .paste-component-button': 'pasteComponent', + 'click .tags-button': 'openManageTags', }, options: { @@ -97,6 +99,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 @@ -125,6 +133,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); @@ -150,6 +159,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(); @@ -409,6 +419,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 fc7f807257ca..b4ee286ae897 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'; @@ -295,11 +296,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..107f79f04470 --- /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/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 2f83c0c6d5e1..ee2b56860187 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -213,7 +213,10 @@
${_("Location ID")}
)}

- + + % if show_unit_tags: + + % endif % endif @@ -238,4 +241,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 a027cc764bd7..38a61ff49106 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -7,13 +7,14 @@ 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, use_video_gallery_flow +from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, use_video_gallery_flow, 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_new_video_gallery_flow = use_video_gallery_flow() +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" @@ -118,6 +119,11 @@ ${_("Duplicate")} % endif + % if use_tagging: + + % endif % if can_move: